mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* move watermark options into watermark selector * fix graphics engine overlay performance * more refactoring * add some tests for existing watermark selector behavior * remove extra ffprobe call on all watermarks * remove a bunch of unused code; add failing tests * implement new watermark selection * add tests for new selector * probably sufficient (though verbose) test coverage * more tests * remove some unused code * simplify watermark selection * remove old selection code and tests * more testspull/2329/head
26 changed files with 877 additions and 658 deletions
@ -1,23 +0,0 @@
@@ -1,23 +0,0 @@
|
||||
using ErsatzTV.Core.FFmpeg; |
||||
using NUnit.Framework; |
||||
using Shouldly; |
||||
|
||||
namespace ErsatzTV.Core.Tests.FFmpeg; |
||||
|
||||
[TestFixture] |
||||
public class WatermarkCalculatorTests |
||||
{ |
||||
[Test] |
||||
public void EntireVideoBetweenWatermarks_ShouldReturn_EmptyFadePointList() |
||||
{ |
||||
List<FadePoint> actual = WatermarkCalculator.CalculateFadePoints( |
||||
new DateTimeOffset(2022, 01, 31, 13, 34, 00, TimeSpan.FromHours(-5)), |
||||
TimeSpan.Zero, |
||||
TimeSpan.FromMinutes(5), |
||||
None, |
||||
15, |
||||
10); |
||||
|
||||
actual.Count.ShouldBe(0); |
||||
} |
||||
} |
||||
@ -0,0 +1,352 @@
@@ -0,0 +1,352 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Core.FFmpeg; |
||||
using ErsatzTV.Core.Interfaces.Images; |
||||
using Microsoft.Extensions.Logging; |
||||
using NSubstitute; |
||||
using NUnit.Framework; |
||||
using Serilog; |
||||
using Shouldly; |
||||
|
||||
namespace ErsatzTV.Core.Tests.FFmpeg; |
||||
|
||||
[TestFixture] |
||||
public class WatermarkSelectorTests |
||||
{ |
||||
private static readonly WatermarkSelector WatermarkSelector; |
||||
private static readonly Option<ChannelWatermark> WatermarkNone; |
||||
private static readonly ChannelWatermark WatermarkGlobal; |
||||
private static readonly ChannelWatermark WatermarkChannel; |
||||
private static readonly ChannelWatermark WatermarkPlayoutItem; |
||||
private static readonly ChannelWatermark WatermarkTemplateDeco; |
||||
private static readonly ChannelWatermark WatermarkDefaultDeco; |
||||
private static readonly Channel ChannelWithWatermark; |
||||
private static readonly Channel ChannelNoWatermark; |
||||
private static readonly DateTimeOffset Now = new(2025, 08, 17, 12, 0, 0, TimeSpan.FromHours(-5)); |
||||
|
||||
private static readonly PlayoutItem PlayoutItemDisableWatermarks = |
||||
new() { Watermarks = [], DisableWatermarks = true }; |
||||
|
||||
private static readonly PlayoutItem PlayoutItemWithNoWatermarks = |
||||
new() { Watermarks = [], DisableWatermarks = false }; |
||||
|
||||
private static readonly PlayoutItem PlayoutItemWithWatermark; |
||||
|
||||
private static readonly PlayoutItem PlayoutItemWithDisabledWatermark; |
||||
|
||||
private static readonly PlayoutItem TemplateDecoInherit; |
||||
private static readonly PlayoutItem TemplateDecoDisable; |
||||
private static readonly PlayoutItem TemplateDecoInheritWithWatermark; |
||||
private static readonly PlayoutItem TemplateDecoDisableWithWatermark; |
||||
private static readonly PlayoutItem TemplateDecoOverrideWithWatermark; |
||||
private static readonly PlayoutItem TemplateDecoInheritDefaultDecoOverrideWithWatermark; |
||||
|
||||
private static readonly PlayoutItem DefaultDecoInherit; |
||||
private static readonly PlayoutItem DefaultDecoDisable; |
||||
private static readonly PlayoutItem DefaultDecoInheritWithWatermark; |
||||
private static readonly PlayoutItem DefaultDecoDisableWithWatermark; |
||||
private static readonly PlayoutItem DefaultDecoOverrideWithWatermark; |
||||
|
||||
private static readonly List<ChannelWatermark> WatermarkResultEmpty = []; |
||||
|
||||
static WatermarkSelectorTests() |
||||
{ |
||||
Log.Logger = new LoggerConfiguration() |
||||
.MinimumLevel.Debug() |
||||
.WriteTo.Console() |
||||
.CreateLogger(); |
||||
|
||||
var loggerFactory = new LoggerFactory().AddSerilog(Log.Logger); |
||||
|
||||
WatermarkSelector = new WatermarkSelector( |
||||
Substitute.For<IImageCache>(), |
||||
loggerFactory.CreateLogger<WatermarkSelector>()); |
||||
|
||||
WatermarkNone = Option<ChannelWatermark>.None; |
||||
|
||||
WatermarkGlobal = new ChannelWatermark { Id = 0, Name = "Global", Image = "GlobalImage" }; |
||||
|
||||
WatermarkChannel = new ChannelWatermark { Id = 1, Name = "Channel", Image = "ChannelImage" }; |
||||
WatermarkPlayoutItem = new ChannelWatermark { Id = 2, Name = "PlayoutItem", Image = "PlayoutItemImage" }; |
||||
WatermarkTemplateDeco = new ChannelWatermark { Id = 3, Name = "TemplateDeco", Image = "TemplateDecoImage" }; |
||||
WatermarkDefaultDeco = new ChannelWatermark { Id = 4, Name = "DefaultDeco", Image = "DefaultDecoImage" }; |
||||
|
||||
ChannelWithWatermark = new Channel(Guid.Empty) |
||||
{ Id = 0, Watermark = WatermarkChannel, WatermarkId = WatermarkChannel.Id }; |
||||
|
||||
ChannelNoWatermark = new Channel(Guid.Empty) { Id = 0, Watermark = null, WatermarkId = null }; |
||||
|
||||
PlayoutItemWithWatermark = new PlayoutItem { Watermarks = [WatermarkPlayoutItem], DisableWatermarks = false }; |
||||
|
||||
PlayoutItemWithDisabledWatermark = new PlayoutItem |
||||
{ Watermarks = [WatermarkPlayoutItem], DisableWatermarks = true }; |
||||
|
||||
var decoWithInherit = new DecoTemplate |
||||
{ |
||||
Items = [new DecoTemplateItem { Deco = new Deco { WatermarkMode = DecoMode.Inherit } }] |
||||
}; |
||||
|
||||
var decoWithDisable = new DecoTemplate |
||||
{ |
||||
Items = [new DecoTemplateItem { Deco = new Deco { WatermarkMode = DecoMode.Disable } }] |
||||
}; |
||||
|
||||
var decoWithOverride = new DecoTemplate |
||||
{ |
||||
Items = |
||||
[ |
||||
new DecoTemplateItem |
||||
{ |
||||
Deco = new Deco |
||||
{ |
||||
WatermarkMode = DecoMode.Override, |
||||
DecoWatermarks = [new DecoWatermark { Watermark = WatermarkTemplateDeco }] |
||||
} |
||||
} |
||||
] |
||||
}; |
||||
|
||||
var playoutWithTemplateDecoInherit = new Playout |
||||
{ |
||||
Templates = |
||||
[ |
||||
new PlayoutTemplate |
||||
{ |
||||
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(), |
||||
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(), |
||||
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(), |
||||
DecoTemplate = decoWithInherit |
||||
} |
||||
] |
||||
}; |
||||
|
||||
var playoutWithTemplateDecoDisable = new Playout |
||||
{ |
||||
Templates = |
||||
[ |
||||
new PlayoutTemplate |
||||
{ |
||||
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(), |
||||
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(), |
||||
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(), |
||||
DecoTemplate = decoWithDisable |
||||
} |
||||
] |
||||
}; |
||||
|
||||
var playoutWithTemplateDecoOverride = new Playout |
||||
{ |
||||
Templates = |
||||
[ |
||||
new PlayoutTemplate |
||||
{ |
||||
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(), |
||||
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(), |
||||
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(), |
||||
DecoTemplate = decoWithOverride |
||||
} |
||||
] |
||||
}; |
||||
|
||||
TemplateDecoInherit = new PlayoutItem { Playout = playoutWithTemplateDecoInherit, Watermarks = [] }; |
||||
|
||||
TemplateDecoDisable = new PlayoutItem |
||||
{ Watermarks = [], DisableWatermarks = false, Playout = playoutWithTemplateDecoDisable }; |
||||
|
||||
TemplateDecoInheritWithWatermark = new PlayoutItem |
||||
{ |
||||
Watermarks = [WatermarkPlayoutItem], DisableWatermarks = false, Playout = playoutWithTemplateDecoInherit |
||||
}; |
||||
|
||||
TemplateDecoDisableWithWatermark = new PlayoutItem |
||||
{ |
||||
Watermarks = [WatermarkPlayoutItem], DisableWatermarks = false, Playout = playoutWithTemplateDecoDisable |
||||
}; |
||||
|
||||
TemplateDecoOverrideWithWatermark = new PlayoutItem |
||||
{ |
||||
Watermarks = [WatermarkPlayoutItem], DisableWatermarks = false, Playout = playoutWithTemplateDecoOverride |
||||
}; |
||||
|
||||
var playoutWithDecoInherit = new Playout |
||||
{ |
||||
Deco = new Deco { WatermarkMode = DecoMode.Inherit }, |
||||
Templates = [] |
||||
}; |
||||
|
||||
var playoutWithDecoDisable = new Playout |
||||
{ |
||||
Deco = new Deco { WatermarkMode = DecoMode.Disable }, |
||||
Templates = [] |
||||
}; |
||||
|
||||
var playoutWithDecoOverride = new Playout |
||||
{ |
||||
Deco = new Deco |
||||
{ |
||||
WatermarkMode = DecoMode.Override, |
||||
DecoWatermarks = [new DecoWatermark { Watermark = WatermarkDefaultDeco }] |
||||
}, |
||||
Templates = [] |
||||
}; |
||||
|
||||
DefaultDecoInherit = new PlayoutItem { Playout = playoutWithDecoInherit, Watermarks = [] }; |
||||
|
||||
DefaultDecoDisable = new PlayoutItem |
||||
{ Watermarks = [], DisableWatermarks = false, Playout = playoutWithDecoDisable }; |
||||
|
||||
DefaultDecoInheritWithWatermark = new PlayoutItem |
||||
{ |
||||
Watermarks = [WatermarkPlayoutItem], DisableWatermarks = false, Playout = playoutWithDecoInherit |
||||
}; |
||||
|
||||
DefaultDecoDisableWithWatermark = new PlayoutItem |
||||
{ |
||||
Watermarks = [WatermarkPlayoutItem], DisableWatermarks = false, Playout = playoutWithDecoDisable |
||||
}; |
||||
|
||||
DefaultDecoOverrideWithWatermark = new PlayoutItem |
||||
{ |
||||
Watermarks = [WatermarkPlayoutItem], DisableWatermarks = false, Playout = playoutWithDecoOverride |
||||
}; |
||||
|
||||
var playoutWithTemplateDecoInheritDefaultDecoOverride = new Playout |
||||
{ |
||||
Templates = |
||||
[ |
||||
new PlayoutTemplate |
||||
{ |
||||
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(), |
||||
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(), |
||||
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(), |
||||
DecoTemplate = decoWithInherit |
||||
} |
||||
], |
||||
Deco = new Deco |
||||
{ |
||||
WatermarkMode = DecoMode.Override, |
||||
DecoWatermarks = [new DecoWatermark { Watermark = WatermarkDefaultDeco }] |
||||
}, |
||||
}; |
||||
|
||||
TemplateDecoInheritDefaultDecoOverrideWithWatermark = new PlayoutItem |
||||
{ |
||||
Watermarks = [WatermarkPlayoutItem], |
||||
DisableWatermarks = false, |
||||
Playout = playoutWithTemplateDecoInheritDefaultDecoOverride |
||||
}; |
||||
|
||||
} |
||||
|
||||
private static IEnumerable<(Option<ChannelWatermark>, Channel, PlayoutItem, List<ChannelWatermark>)> |
||||
SelectWatermarksTestCases() |
||||
{ |
||||
// STANDARD --------------------------------------------
|
||||
|
||||
// no watermark when none are configured
|
||||
yield return (WatermarkNone, ChannelNoWatermark, PlayoutItemWithNoWatermarks, WatermarkResultEmpty); |
||||
|
||||
// no watermark when global configured but disabled playout item
|
||||
yield return (WatermarkGlobal, ChannelNoWatermark, PlayoutItemDisableWatermarks, WatermarkResultEmpty); |
||||
|
||||
// global watermark when global configured
|
||||
yield return (WatermarkGlobal, ChannelNoWatermark, PlayoutItemWithNoWatermarks, [WatermarkGlobal]); |
||||
|
||||
// channel watermark when global and channel configured
|
||||
yield return (WatermarkGlobal, ChannelWithWatermark, PlayoutItemWithNoWatermarks, [WatermarkChannel]); |
||||
|
||||
// channel watermark when channel configured
|
||||
yield return (WatermarkNone, ChannelWithWatermark, PlayoutItemWithNoWatermarks, [WatermarkChannel]); |
||||
|
||||
// playout item when global, channel and playout item configured
|
||||
yield return (WatermarkGlobal, ChannelWithWatermark, PlayoutItemWithWatermark, [WatermarkPlayoutItem]); |
||||
|
||||
// playout item when channel and playout item configured
|
||||
yield return (WatermarkNone, ChannelWithWatermark, PlayoutItemWithWatermark, [WatermarkPlayoutItem]); |
||||
|
||||
// playout item when playout item configured
|
||||
yield return (WatermarkNone, ChannelNoWatermark, PlayoutItemWithWatermark, [WatermarkPlayoutItem]); |
||||
|
||||
// no watermark when channel configured with playout item disabled
|
||||
yield return (WatermarkNone, ChannelWithWatermark, PlayoutItemDisableWatermarks, WatermarkResultEmpty); |
||||
|
||||
// no watermark when global and channel configured with playout item disabled
|
||||
yield return (WatermarkGlobal, ChannelWithWatermark, PlayoutItemDisableWatermarks, WatermarkResultEmpty); |
||||
|
||||
// no watermark when global, channel and playout item configured with playout item disabled
|
||||
yield return (WatermarkGlobal, ChannelWithWatermark, PlayoutItemWithDisabledWatermark, WatermarkResultEmpty); |
||||
|
||||
// PLAYOUT TEMPLATE DECO -------------------------------
|
||||
|
||||
// no watermark when global, channel and playout item configured with template deco disabled
|
||||
yield return (WatermarkGlobal, ChannelWithWatermark, TemplateDecoDisableWithWatermark, WatermarkResultEmpty); |
||||
|
||||
// no watermark when global, channel configured with template deco disabled
|
||||
yield return (WatermarkGlobal, ChannelWithWatermark, TemplateDecoDisable, WatermarkResultEmpty); |
||||
|
||||
// no watermark when global configured with template deco disabled
|
||||
yield return (WatermarkGlobal, ChannelNoWatermark, TemplateDecoDisable, WatermarkResultEmpty); |
||||
|
||||
// playout item when global, channel and playout item configured with template deco inherit
|
||||
yield return (WatermarkGlobal, ChannelWithWatermark, TemplateDecoInheritWithWatermark, [WatermarkPlayoutItem]); |
||||
|
||||
// channel when global, channel configured with template deco inherit
|
||||
yield return (WatermarkGlobal, ChannelWithWatermark, TemplateDecoInherit, [WatermarkChannel]); |
||||
|
||||
// global when global configured with template deco inherit
|
||||
yield return (WatermarkGlobal, ChannelNoWatermark, TemplateDecoInherit, [WatermarkGlobal]); |
||||
|
||||
// no watermark when none configured with template deco inherit
|
||||
yield return (WatermarkNone, ChannelNoWatermark, TemplateDecoInherit, WatermarkResultEmpty); |
||||
|
||||
// PLAYOUT DEFAULT DECO --------------------------------
|
||||
|
||||
// no watermark when global, channel and playout item configured with default deco disabled
|
||||
yield return (WatermarkGlobal, ChannelWithWatermark, DefaultDecoDisableWithWatermark, WatermarkResultEmpty); |
||||
|
||||
// no watermark when global, channel configured with default deco disabled
|
||||
yield return (WatermarkGlobal, ChannelWithWatermark, DefaultDecoDisable, WatermarkResultEmpty); |
||||
|
||||
// no watermark when global configured with default deco disabled
|
||||
yield return (WatermarkGlobal, ChannelNoWatermark, DefaultDecoDisable, WatermarkResultEmpty); |
||||
|
||||
// playout item when global, channel and playout item configured with default deco inherit
|
||||
yield return (WatermarkGlobal, ChannelWithWatermark, DefaultDecoInheritWithWatermark, [WatermarkPlayoutItem]); |
||||
|
||||
// channel when global, channel configured with default deco inherit
|
||||
yield return (WatermarkGlobal, ChannelWithWatermark, DefaultDecoInherit, [WatermarkChannel]); |
||||
|
||||
// global when global configured with default deco inherit
|
||||
yield return (WatermarkGlobal, ChannelNoWatermark, DefaultDecoInherit, [WatermarkGlobal]); |
||||
|
||||
// no watermark when none configured with default deco inherit
|
||||
yield return (WatermarkNone, ChannelNoWatermark, DefaultDecoInherit, WatermarkResultEmpty); |
||||
|
||||
// PLAYOUT TEMPLATE AND DEFAULT DECO -------------------
|
||||
|
||||
// default deco when global, channel and playout item configured with default deco override, template deco inherit
|
||||
yield return (WatermarkGlobal, ChannelWithWatermark, TemplateDecoInheritDefaultDecoOverrideWithWatermark, [WatermarkDefaultDeco]); |
||||
} |
||||
|
||||
[TestCaseSource(nameof(SelectWatermarksTestCases))] |
||||
public void Should_Select_Appropriate_Watermark( |
||||
(Option<ChannelWatermark> globalWatermark, |
||||
Channel channel, |
||||
PlayoutItem playoutItem, |
||||
List<ChannelWatermark> expectedWatermarks) td) |
||||
{ |
||||
List<WatermarkOptions> watermarks = WatermarkSelector.SelectWatermarks( |
||||
td.globalWatermark, |
||||
td.channel, |
||||
td.playoutItem, |
||||
Now); |
||||
|
||||
watermarks.Count.ShouldBe(td.expectedWatermarks.Count); |
||||
for (var i = 0; i < td.expectedWatermarks.Count; i++) |
||||
{ |
||||
watermarks[i].Watermark.ShouldBe(td.expectedWatermarks[i]); |
||||
} |
||||
} |
||||
|
||||
// TODO: also decos?
|
||||
} |
||||
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Core.Scheduling; |
||||
|
||||
namespace ErsatzTV.Core.FFmpeg; |
||||
|
||||
public static class DecoSelector |
||||
{ |
||||
public static DecoEntries GetDecoEntries(Playout playout, DateTimeOffset now) |
||||
{ |
||||
if (playout is null) |
||||
{ |
||||
return new DecoEntries(Option<Deco>.None, Option<Deco>.None); |
||||
} |
||||
|
||||
Option<Deco> maybePlayoutDeco = Optional(playout.Deco); |
||||
Option<Deco> maybeTemplateDeco = Option<Deco>.None; |
||||
|
||||
Option<PlayoutTemplate> maybeActiveTemplate = |
||||
PlayoutTemplateSelector.GetPlayoutTemplateFor(playout.Templates, now); |
||||
|
||||
foreach (PlayoutTemplate activeTemplate in maybeActiveTemplate) |
||||
{ |
||||
Option<DecoTemplateItem> maybeItem = Optional(activeTemplate.DecoTemplate) |
||||
.SelectMany(dt => dt.Items) |
||||
.Find(i => i.StartTime <= now.TimeOfDay && i.EndTime == TimeSpan.Zero || i.EndTime > now.TimeOfDay); |
||||
foreach (DecoTemplateItem item in maybeItem) |
||||
{ |
||||
maybeTemplateDeco = Optional(item.Deco); |
||||
} |
||||
} |
||||
|
||||
return new DecoEntries(maybeTemplateDeco, maybePlayoutDeco); |
||||
} |
||||
} |
||||
|
||||
public sealed record DecoEntries(Option<Deco> TemplateDeco, Option<Deco> PlayoutDeco); |
||||
@ -1,79 +0,0 @@
@@ -1,79 +0,0 @@
|
||||
namespace ErsatzTV.Core.FFmpeg; |
||||
|
||||
public static class WatermarkCalculator |
||||
{ |
||||
public static List<FadePoint> CalculateFadePoints( |
||||
DateTimeOffset itemStartTime, |
||||
TimeSpan inPoint, |
||||
TimeSpan outPoint, |
||||
Option<TimeSpan> streamSeek, |
||||
int frequencyMinutes, |
||||
int durationSeconds) |
||||
{ |
||||
var result = new List<FadePoint>(); |
||||
|
||||
TimeSpan duration = outPoint - inPoint; |
||||
DateTimeOffset itemFinishTime = itemStartTime + duration; |
||||
|
||||
DateTimeOffset start = itemStartTime.AddMinutes(-16); |
||||
|
||||
// find the next whole minute
|
||||
if (start.Second > 0 || start.Millisecond > 0) |
||||
{ |
||||
start = start.AddMinutes(1); |
||||
start = start.AddSeconds(-start.Second); |
||||
start = start.AddMilliseconds(-start.Millisecond); |
||||
} |
||||
|
||||
DateTimeOffset finish = itemFinishTime; |
||||
|
||||
// find the previous whole minute
|
||||
if (finish.Second > 0 || finish.Millisecond > 0) |
||||
{ |
||||
finish = finish.AddSeconds(-finish.Second); |
||||
finish = finish.AddMilliseconds(-finish.Millisecond); |
||||
} |
||||
|
||||
DateTimeOffset current = start; |
||||
while (current <= finish) |
||||
{ |
||||
current = current.AddMinutes(1); |
||||
if (current.Minute % frequencyMinutes == 0) |
||||
{ |
||||
TimeSpan fadeInTime = inPoint + (current - itemStartTime); |
||||
|
||||
result.Add(new FadeInPoint(fadeInTime)); |
||||
result.Add(new FadeOutPoint(fadeInTime.Add(TimeSpan.FromSeconds(durationSeconds)))); |
||||
} |
||||
} |
||||
|
||||
// if we're seeking, subtract the seek from each item and return that
|
||||
foreach (TimeSpan ss in streamSeek) |
||||
{ |
||||
result = result.Map(fp => fp with { Time = fp.Time - ss }).ToList(); |
||||
} |
||||
|
||||
// trim points that have already passed
|
||||
result.RemoveAll(fp => fp.Time < TimeSpan.Zero); |
||||
|
||||
// trim points that are past the end
|
||||
result.RemoveAll(fp => fp.Time >= outPoint); |
||||
|
||||
if (result.Count != 0) |
||||
{ |
||||
for (var i = 0; i < result.Count; i++) |
||||
{ |
||||
result[i].EnableStart = i == 0 ? TimeSpan.Zero : result[i - 1].Time.Add(TimeSpan.FromSeconds(1)); |
||||
} |
||||
|
||||
for (var i = 0; i < result.Count; i++) |
||||
{ |
||||
result[i].EnableFinish = i == result.Count - 1 |
||||
? outPoint |
||||
: result[i + 1].Time.Subtract(TimeSpan.FromSeconds(1)); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
@ -0,0 +1,298 @@
@@ -0,0 +1,298 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Filler; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Core.Images; |
||||
using ErsatzTV.Core.Interfaces.FFmpeg; |
||||
using ErsatzTV.Core.Interfaces.Images; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.Core.FFmpeg; |
||||
|
||||
public class WatermarkSelector(IImageCache imageCache, ILogger<WatermarkSelector> logger) |
||||
: IWatermarkSelector |
||||
{ |
||||
public List<WatermarkOptions> SelectWatermarks( |
||||
Option<ChannelWatermark> globalWatermark, |
||||
Channel channel, |
||||
PlayoutItem playoutItem, |
||||
DateTimeOffset now) |
||||
{ |
||||
var result = new List<WatermarkOptions>(); |
||||
|
||||
if (channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect) |
||||
{ |
||||
return result; |
||||
} |
||||
|
||||
if (playoutItem.DisableWatermarks) |
||||
{ |
||||
logger.LogDebug("Watermark is disabled by playout item"); |
||||
return result; |
||||
} |
||||
|
||||
DecoEntries decoEntries = DecoSelector.GetDecoEntries(playoutItem.Playout, now); |
||||
|
||||
// first, check deco template / active deco
|
||||
foreach (Deco templateDeco in decoEntries.TemplateDeco) |
||||
{ |
||||
switch (templateDeco.WatermarkMode) |
||||
{ |
||||
case DecoMode.Override: |
||||
if (playoutItem.FillerKind is FillerKind.None || templateDeco.UseWatermarkDuringFiller) |
||||
{ |
||||
logger.LogDebug("Watermark will come from template deco (override)"); |
||||
result.AddRange( |
||||
OptionsForWatermarks(channel, templateDeco.DecoWatermarks.Map(dwm => dwm.Watermark))); |
||||
return result; |
||||
} |
||||
|
||||
logger.LogDebug("Watermark is disabled by template deco during filler"); |
||||
return result; |
||||
case DecoMode.Disable: |
||||
logger.LogDebug("Watermark is disabled by template deco"); |
||||
return result; |
||||
case DecoMode.Inherit: |
||||
logger.LogDebug("Watermark will inherit from playout deco"); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
// second, check playout deco
|
||||
foreach (Deco playoutDeco in decoEntries.PlayoutDeco) |
||||
{ |
||||
switch (playoutDeco.WatermarkMode) |
||||
{ |
||||
case DecoMode.Override: |
||||
if (playoutItem.FillerKind is FillerKind.None || playoutDeco.UseWatermarkDuringFiller) |
||||
{ |
||||
logger.LogDebug("Watermark will come from playout deco (override)"); |
||||
result.AddRange( |
||||
OptionsForWatermarks(channel, playoutDeco.DecoWatermarks.Map(dwm => dwm.Watermark))); |
||||
return result; |
||||
} |
||||
|
||||
logger.LogDebug("Watermark is disabled by playout deco during filler"); |
||||
return result; |
||||
case DecoMode.Disable: |
||||
logger.LogDebug("Watermark is disabled by playout deco"); |
||||
return result; |
||||
case DecoMode.Inherit: |
||||
logger.LogDebug("Watermark will inherit from channel and/or global setting"); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (playoutItem.Watermarks.Count > 0) |
||||
{ |
||||
foreach (var watermark in playoutItem.Watermarks) |
||||
{ |
||||
var options = GetWatermarkOptions(channel, watermark, Option<ChannelWatermark>.None); |
||||
result.AddRange(options); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
|
||||
var finalOptions = GetWatermarkOptions(channel, Option<ChannelWatermark>.None, globalWatermark); |
||||
result.AddRange(finalOptions); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
public Option<WatermarkOptions> GetWatermarkOptions( |
||||
Channel channel, |
||||
Option<ChannelWatermark> playoutItemWatermark, |
||||
Option<ChannelWatermark> globalWatermark) |
||||
{ |
||||
if (channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect) |
||||
{ |
||||
return Option<WatermarkOptions>.None; |
||||
} |
||||
|
||||
// check for playout item watermark
|
||||
foreach (ChannelWatermark watermark in playoutItemWatermark) |
||||
{ |
||||
switch (watermark.ImageSource) |
||||
{ |
||||
// used for song progress overlay
|
||||
case ChannelWatermarkImageSource.Resource: |
||||
return new WatermarkOptions( |
||||
watermark, |
||||
Path.Combine(FileSystemLayout.ResourcesCacheFolder, watermark.Image), |
||||
Option<int>.None); |
||||
case ChannelWatermarkImageSource.Custom: |
||||
// bad form validation makes this possible
|
||||
if (string.IsNullOrWhiteSpace(watermark.Image)) |
||||
{ |
||||
logger.LogWarning( |
||||
"Watermark {Name} has custom image configured with no image; ignoring", |
||||
watermark.Name); |
||||
break; |
||||
} |
||||
|
||||
logger.LogDebug("Watermark will come from playout item (custom)"); |
||||
|
||||
string customPath = imageCache.GetPathForImage( |
||||
watermark.Image, |
||||
ArtworkKind.Watermark, |
||||
Option<int>.None); |
||||
return new WatermarkOptions( |
||||
watermark, |
||||
customPath, |
||||
None); |
||||
case ChannelWatermarkImageSource.ChannelLogo: |
||||
logger.LogDebug("Watermark will come from playout item (channel logo)"); |
||||
|
||||
string channelPath = ChannelLogoGenerator.GenerateChannelLogoUrl(channel); |
||||
Option<Artwork> maybeLogoArtwork = |
||||
Optional(channel.Artwork.Find(a => a.ArtworkKind == ArtworkKind.Logo)); |
||||
foreach (var logoArtwork in maybeLogoArtwork) |
||||
{ |
||||
channelPath = Artwork.IsExternalUrl(logoArtwork.Path) |
||||
? logoArtwork.Path |
||||
: imageCache.GetPathForImage(logoArtwork.Path, ArtworkKind.Logo, Option<int>.None); |
||||
} |
||||
|
||||
return new WatermarkOptions(watermark, channelPath, None); |
||||
default: |
||||
throw new NotSupportedException("Unsupported watermark image source"); |
||||
} |
||||
} |
||||
|
||||
// check for channel watermark
|
||||
if (channel.Watermark != null) |
||||
{ |
||||
switch (channel.Watermark.ImageSource) |
||||
{ |
||||
case ChannelWatermarkImageSource.Custom: |
||||
logger.LogDebug("Watermark will come from channel (custom)"); |
||||
|
||||
string customPath = imageCache.GetPathForImage( |
||||
channel.Watermark.Image, |
||||
ArtworkKind.Watermark, |
||||
Option<int>.None); |
||||
return new WatermarkOptions( |
||||
channel.Watermark, |
||||
customPath, |
||||
None); |
||||
case ChannelWatermarkImageSource.ChannelLogo: |
||||
logger.LogDebug("Watermark will come from channel (channel logo)"); |
||||
|
||||
string channelPath = ChannelLogoGenerator.GenerateChannelLogoUrl(channel); |
||||
Option<Artwork> maybeLogoArtwork = |
||||
Optional(channel.Artwork.Find(a => a.ArtworkKind == ArtworkKind.Logo)); |
||||
foreach (var logoArtwork in maybeLogoArtwork) |
||||
{ |
||||
channelPath = Artwork.IsExternalUrl(logoArtwork.Path) |
||||
? logoArtwork.Path |
||||
: imageCache.GetPathForImage(logoArtwork.Path, ArtworkKind.Logo, Option<int>.None); |
||||
} |
||||
|
||||
return new WatermarkOptions(channel.Watermark, channelPath, None); |
||||
default: |
||||
throw new NotSupportedException("Unsupported watermark image source"); |
||||
} |
||||
} |
||||
|
||||
// check for global watermark
|
||||
foreach (ChannelWatermark watermark in globalWatermark) |
||||
{ |
||||
switch (watermark.ImageSource) |
||||
{ |
||||
case ChannelWatermarkImageSource.Custom: |
||||
logger.LogDebug("Watermark will come from global (custom)"); |
||||
|
||||
string customPath = imageCache.GetPathForImage( |
||||
watermark.Image, |
||||
ArtworkKind.Watermark, |
||||
Option<int>.None); |
||||
return new WatermarkOptions( |
||||
watermark, |
||||
customPath, |
||||
None); |
||||
case ChannelWatermarkImageSource.ChannelLogo: |
||||
logger.LogDebug("Watermark will come from global (channel logo)"); |
||||
|
||||
string channelPath = ChannelLogoGenerator.GenerateChannelLogoUrl(channel); |
||||
Option<Artwork> maybeLogoArtwork = |
||||
Optional(channel.Artwork.Find(a => a.ArtworkKind == ArtworkKind.Logo)); |
||||
foreach (var logoArtwork in maybeLogoArtwork) |
||||
{ |
||||
channelPath = Artwork.IsExternalUrl(logoArtwork.Path) |
||||
? logoArtwork.Path |
||||
: imageCache.GetPathForImage(logoArtwork.Path, ArtworkKind.Logo, Option<int>.None); |
||||
} |
||||
|
||||
return new WatermarkOptions(watermark, channelPath, None); |
||||
default: |
||||
throw new NotSupportedException("Unsupported watermark image source"); |
||||
} |
||||
} |
||||
|
||||
return Option<WatermarkOptions>.None; |
||||
} |
||||
|
||||
private List<WatermarkOptions> OptionsForWatermarks(Channel channel, IEnumerable<ChannelWatermark> watermarks) |
||||
{ |
||||
var result = new List<WatermarkOptions>(); |
||||
|
||||
foreach (var watermark in watermarks) |
||||
{ |
||||
result.AddRange(GetWatermarkOptions(channel, watermark)); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private Option<WatermarkOptions> GetWatermarkOptions(Channel channel, ChannelWatermark watermark) |
||||
{ |
||||
switch (watermark.ImageSource) |
||||
{ |
||||
// used for song progress overlay
|
||||
case ChannelWatermarkImageSource.Resource: |
||||
return new WatermarkOptions( |
||||
watermark, |
||||
Path.Combine(FileSystemLayout.ResourcesCacheFolder, watermark.Image), |
||||
Option<int>.None); |
||||
case ChannelWatermarkImageSource.Custom: |
||||
// bad form validation makes this possible
|
||||
if (string.IsNullOrWhiteSpace(watermark.Image)) |
||||
{ |
||||
logger.LogWarning( |
||||
"Watermark {Name} has custom image configured with no image; ignoring", |
||||
watermark.Name); |
||||
break; |
||||
} |
||||
|
||||
logger.LogDebug("Watermark will come from playout item (custom)"); |
||||
|
||||
string customPath = imageCache.GetPathForImage( |
||||
watermark.Image, |
||||
ArtworkKind.Watermark, |
||||
Option<int>.None); |
||||
return new WatermarkOptions( |
||||
watermark, |
||||
customPath, |
||||
None); |
||||
case ChannelWatermarkImageSource.ChannelLogo: |
||||
logger.LogDebug("Watermark will come from playout item (channel logo)"); |
||||
|
||||
string channelPath = ChannelLogoGenerator.GenerateChannelLogoUrl(channel); |
||||
Option<Artwork> maybeLogoArtwork = |
||||
Optional(channel.Artwork.Find(a => a.ArtworkKind == ArtworkKind.Logo)); |
||||
foreach (var logoArtwork in maybeLogoArtwork) |
||||
{ |
||||
channelPath = Artwork.IsExternalUrl(logoArtwork.Path) |
||||
? logoArtwork.Path |
||||
: imageCache.GetPathForImage(logoArtwork.Path, ArtworkKind.Logo, Option<int>.None); |
||||
} |
||||
|
||||
return new WatermarkOptions(watermark, channelPath, None); |
||||
default: |
||||
throw new NotSupportedException("Unsupported watermark image source"); |
||||
} |
||||
|
||||
return Option<WatermarkOptions>.None; |
||||
} |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.FFmpeg; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.FFmpeg; |
||||
|
||||
public interface IWatermarkSelector |
||||
{ |
||||
List<WatermarkOptions> SelectWatermarks( |
||||
Option<ChannelWatermark> globalWatermark, |
||||
Channel channel, |
||||
PlayoutItem playoutItem, |
||||
DateTimeOffset now); |
||||
|
||||
Option<WatermarkOptions> GetWatermarkOptions( |
||||
Channel channel, |
||||
Option<ChannelWatermark> playoutItemWatermark, |
||||
Option<ChannelWatermark> globalWatermark); |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.FFmpeg; |
||||
|
||||
public abstract record WatermarkResult; |
||||
|
||||
public sealed record InheritWatermark : WatermarkResult; |
||||
|
||||
public sealed record DisableWatermark : WatermarkResult; |
||||
|
||||
public sealed record CustomWatermarks(List<ChannelWatermark> Watermarks) : WatermarkResult; |
||||
Loading…
Reference in new issue