support multiple watermarks in yaml schedules (#2267)
* add multiple watermarks per playout item * fixes * update yaml playout watermark to support multiple watermarks * use graphics engine for intermittent watermarks
This commit is contained in:
+8
-3
@@ -6,9 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
- Add *experimental* graphics engine
|
- Add *experimental* graphics engine
|
||||||
- `Permanent` watermarks will use new graphics engine
|
- All watermarks will use new graphics engine
|
||||||
- `Opacity Expression` watermarks will use new graphics engine
|
|
||||||
- `Intermittent` watermarks will still use normal overlay pipeline (for now)
|
|
||||||
- Add `Opacity Expression` watermark mode
|
- Add `Opacity Expression` watermark mode
|
||||||
- This allows specifying an expression that returns an opacity between 0.0 and 1.0
|
- This allows specifying an expression that returns an opacity between 0.0 and 1.0
|
||||||
- The expression can use:
|
- The expression can use:
|
||||||
@@ -16,6 +14,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
- `channel_seconds` - the total number of seconds the frame is from when the channel started/activated
|
- `channel_seconds` - the total number of seconds the frame is from when the channel started/activated
|
||||||
- `time_of_day_seconds` - the total number of seconds the frame is since midnight
|
- `time_of_day_seconds` - the total number of seconds the frame is since midnight
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Allow multiple watermarks on a single playout item
|
||||||
|
- YAML playout: `watermark` instruction changes:
|
||||||
|
- When value is `true`, will add named watermark to list of active watermarks
|
||||||
|
- When value is `false` and `name` is specified, will remove named watermark from list of active watermarks
|
||||||
|
- When value is `false` and `name` is not specified, will clear all active watermarks
|
||||||
|
|
||||||
## [25.4.0] - 2025-08-05
|
## [25.4.0] - 2025-08-05
|
||||||
### Added
|
### Added
|
||||||
- Add `Troubleshoot Playback` to overflow menu on all media cards
|
- Add `Troubleshoot Playback` to overflow menu on all media cards
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
|||||||
.Include(p => p.Channel)
|
.Include(p => p.Channel)
|
||||||
.Include(p => p.Deco)
|
.Include(p => p.Deco)
|
||||||
.Include(p => p.Items)
|
.Include(p => p.Items)
|
||||||
|
.ThenInclude(pi => pi.Watermarks)
|
||||||
.Include(p => p.PlayoutHistory)
|
.Include(p => p.PlayoutHistory)
|
||||||
.Include(p => p.Templates)
|
.Include(p => p.Templates)
|
||||||
.ThenInclude(t => t.Template)
|
.ThenInclude(t => t.Template)
|
||||||
@@ -181,6 +182,10 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
|||||||
.Include(p => p.ProgramScheduleAlternates)
|
.Include(p => p.ProgramScheduleAlternates)
|
||||||
.ThenInclude(a => a.ProgramSchedule)
|
.ThenInclude(a => a.ProgramSchedule)
|
||||||
.ThenInclude(ps => ps.Items)
|
.ThenInclude(ps => ps.Items)
|
||||||
|
.ThenInclude(psi => psi.Watermark)
|
||||||
|
.Include(p => p.ProgramScheduleAlternates)
|
||||||
|
.ThenInclude(a => a.ProgramSchedule)
|
||||||
|
.ThenInclude(ps => ps.Items)
|
||||||
.ThenInclude(psi => psi.Collection)
|
.ThenInclude(psi => psi.Collection)
|
||||||
.Include(p => p.ProgramScheduleAlternates)
|
.Include(p => p.ProgramScheduleAlternates)
|
||||||
.ThenInclude(a => a.ProgramSchedule)
|
.ThenInclude(a => a.ProgramSchedule)
|
||||||
@@ -212,6 +217,9 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
|||||||
.ThenInclude(a => a.MediaItem)
|
.ThenInclude(a => a.MediaItem)
|
||||||
.Include(p => p.ProgramSchedule)
|
.Include(p => p.ProgramSchedule)
|
||||||
.ThenInclude(ps => ps.Items)
|
.ThenInclude(ps => ps.Items)
|
||||||
|
.ThenInclude(psi => psi.Watermark)
|
||||||
|
.Include(p => p.ProgramSchedule)
|
||||||
|
.ThenInclude(ps => ps.Items)
|
||||||
.ThenInclude(psi => psi.Collection)
|
.ThenInclude(psi => psi.Collection)
|
||||||
.Include(p => p.ProgramSchedule)
|
.Include(p => p.ProgramSchedule)
|
||||||
.ThenInclude(ps => ps.Items)
|
.ThenInclude(ps => ps.Items)
|
||||||
|
|||||||
@@ -467,6 +467,7 @@ public class HlsSessionWorker : IHlsSessionWorker
|
|||||||
foreach (var graphicsEngineContext in processModel.GraphicsEngineContext)
|
foreach (var graphicsEngineContext in processModel.GraphicsEngineContext)
|
||||||
{
|
{
|
||||||
var pipe = new Pipe();
|
var pipe = new Pipe();
|
||||||
|
maybePipe = pipe;
|
||||||
processWithPipe = process.WithStandardInputPipe(PipeSource.FromStream(pipe.Reader.AsStream()));
|
processWithPipe = process.WithStandardInputPipe(PipeSource.FromStream(pipe.Reader.AsStream()));
|
||||||
|
|
||||||
// fire and forget graphics engine task
|
// fire and forget graphics engine task
|
||||||
|
|||||||
+12
-8
@@ -164,7 +164,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
|||||||
.ThenInclude(mv => mv.Streams)
|
.ThenInclude(mv => mv.Streams)
|
||||||
.Include(i => i.MediaItem)
|
.Include(i => i.MediaItem)
|
||||||
.ThenInclude(mi => (mi as Image).ImageMetadata)
|
.ThenInclude(mi => (mi as Image).ImageMetadata)
|
||||||
.Include(i => i.Watermark)
|
.Include(i => i.Watermarks)
|
||||||
.Include(i => i.MediaItem)
|
.Include(i => i.MediaItem)
|
||||||
.ThenInclude(mi => (mi as RemoteStream).MediaVersions)
|
.ThenInclude(mi => (mi as RemoteStream).MediaVersions)
|
||||||
.ThenInclude(mv => mv.MediaFiles)
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
@@ -173,7 +173,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
|||||||
.ThenInclude(mv => mv.Streams)
|
.ThenInclude(mv => mv.Streams)
|
||||||
.Include(i => i.MediaItem)
|
.Include(i => i.MediaItem)
|
||||||
.ThenInclude(mi => (mi as RemoteStream).RemoteStreamMetadata)
|
.ThenInclude(mi => (mi as RemoteStream).RemoteStreamMetadata)
|
||||||
.Include(i => i.Watermark)
|
.Include(i => i.Watermarks)
|
||||||
.ForChannelAndTime(channel.Id, now)
|
.ForChannelAndTime(channel.Id, now)
|
||||||
.Map(o => o.ToEither<BaseError>(new UnableToLocatePlayoutItem()))
|
.Map(o => o.ToEither<BaseError>(new UnableToLocatePlayoutItem()))
|
||||||
.BindT(item => ValidatePlayoutItemPath(dbContext, item));
|
.BindT(item => ValidatePlayoutItemPath(dbContext, item));
|
||||||
@@ -242,7 +242,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
|||||||
.BindT(watermarkId => dbContext.ChannelWatermarks
|
.BindT(watermarkId => dbContext.ChannelWatermarks
|
||||||
.SelectOneAsync(w => w.Id, w => w.Id == watermarkId));
|
.SelectOneAsync(w => w.Id, w => w.Id == watermarkId));
|
||||||
|
|
||||||
Option<ChannelWatermark> playoutItemWatermark = Optional(playoutItemWithPath.PlayoutItem.Watermark);
|
List<ChannelWatermark> playoutItemWatermarks = [];
|
||||||
|
playoutItemWatermarks.AddRange(playoutItemWithPath.PlayoutItem.Watermarks);
|
||||||
|
|
||||||
bool disableWatermarks = playoutItemWithPath.PlayoutItem.DisableWatermarks;
|
bool disableWatermarks = playoutItemWithPath.PlayoutItem.DisableWatermarks;
|
||||||
WatermarkResult watermarkResult = GetPlayoutItemWatermark(playoutItemWithPath.PlayoutItem, now);
|
WatermarkResult watermarkResult = GetPlayoutItemWatermark(playoutItemWithPath.PlayoutItem, now);
|
||||||
switch (watermarkResult)
|
switch (watermarkResult)
|
||||||
@@ -254,7 +256,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
|||||||
disableWatermarks = true;
|
disableWatermarks = true;
|
||||||
break;
|
break;
|
||||||
case CustomWatermark watermark:
|
case CustomWatermark watermark:
|
||||||
playoutItemWatermark = watermark.Watermark;
|
playoutItemWatermarks.Clear();
|
||||||
|
playoutItemWatermarks.Add(watermark.Watermark);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +266,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
|||||||
(videoPath, videoVersion) = await _songVideoGenerator.GenerateSongVideo(
|
(videoPath, videoVersion) = await _songVideoGenerator.GenerateSongVideo(
|
||||||
song,
|
song,
|
||||||
channel,
|
channel,
|
||||||
playoutItemWatermark,
|
playoutItemWatermarks.HeadOrNone(),
|
||||||
maybeGlobalWatermark,
|
maybeGlobalWatermark,
|
||||||
ffmpegPath,
|
ffmpegPath,
|
||||||
ffprobePath,
|
ffprobePath,
|
||||||
@@ -278,7 +281,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
|||||||
string image = is43 ? "song_progress_overlay_43.png" : "song_progress_overlay.png";
|
string image = is43 ? "song_progress_overlay_43.png" : "song_progress_overlay.png";
|
||||||
|
|
||||||
disableWatermarks = false;
|
disableWatermarks = false;
|
||||||
playoutItemWatermark = new ChannelWatermark
|
playoutItemWatermarks.Clear();
|
||||||
|
playoutItemWatermarks.Add(new ChannelWatermark
|
||||||
{
|
{
|
||||||
Mode = ChannelWatermarkMode.Permanent,
|
Mode = ChannelWatermarkMode.Permanent,
|
||||||
Size = WatermarkSize.Scaled,
|
Size = WatermarkSize.Scaled,
|
||||||
@@ -289,7 +293,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
|||||||
Location = WatermarkLocation.TopLeft,
|
Location = WatermarkLocation.TopLeft,
|
||||||
ImageSource = ChannelWatermarkImageSource.Resource,
|
ImageSource = ChannelWatermarkImageSource.Resource,
|
||||||
Image = image
|
Image = image
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +339,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
|||||||
start,
|
start,
|
||||||
finish,
|
finish,
|
||||||
effectiveNow,
|
effectiveNow,
|
||||||
playoutItemWatermark,
|
playoutItemWatermarks,
|
||||||
maybeGlobalWatermark,
|
maybeGlobalWatermark,
|
||||||
channel.FFmpegProfile.VaapiDisplay,
|
channel.FFmpegProfile.VaapiDisplay,
|
||||||
channel.FFmpegProfile.VaapiDriver,
|
channel.FFmpegProfile.VaapiDriver,
|
||||||
|
|||||||
+4
-4
@@ -78,11 +78,11 @@ public class PrepareTroubleshootingPlaybackHandler(
|
|||||||
return BaseError.New("Media item does not exist on disk");
|
return BaseError.New("Media item does not exist on disk");
|
||||||
}
|
}
|
||||||
|
|
||||||
Option<ChannelWatermark> maybeWatermark = Option<ChannelWatermark>.None;
|
List<ChannelWatermark> watermarks = [];
|
||||||
if (request.WatermarkId > 0)
|
if (request.WatermarkId > 0)
|
||||||
{
|
{
|
||||||
maybeWatermark = await dbContext.ChannelWatermarks
|
watermarks.AddRange(await dbContext.ChannelWatermarks
|
||||||
.SelectOneAsync(cw => cw.Id, cw => cw.Id == request.WatermarkId);
|
.SelectOneAsync(cw => cw.Id, cw => cw.Id == request.WatermarkId));
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTimeOffset now = DateTimeOffset.Now;
|
DateTimeOffset now = DateTimeOffset.Now;
|
||||||
@@ -132,7 +132,7 @@ public class PrepareTroubleshootingPlaybackHandler(
|
|||||||
now,
|
now,
|
||||||
now + duration,
|
now + duration,
|
||||||
now,
|
now,
|
||||||
maybeWatermark,
|
watermarks,
|
||||||
Option<ChannelWatermark>.None,
|
Option<ChannelWatermark>.None,
|
||||||
ffmpegProfile.VaapiDisplay,
|
ffmpegProfile.VaapiDisplay,
|
||||||
ffmpegProfile.VaapiDriver,
|
ffmpegProfile.VaapiDriver,
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ public class ChannelWatermark
|
|||||||
public int Opacity { get; set; }
|
public int Opacity { get; set; }
|
||||||
public bool PlaceWithinSourceContent { get; set; }
|
public bool PlaceWithinSourceContent { get; set; }
|
||||||
public string OpacityExpression { get; set; }
|
public string OpacityExpression { get; set; }
|
||||||
|
public List<PlayoutItem> PlayoutItems { get; set; }
|
||||||
|
public List<PlayoutItemWatermark> PlayoutItemWatermarks { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ChannelWatermarkMode
|
public enum ChannelWatermarkMode
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ public class PlayoutItem
|
|||||||
public TimeSpan InPoint { get; set; }
|
public TimeSpan InPoint { get; set; }
|
||||||
public TimeSpan OutPoint { get; set; }
|
public TimeSpan OutPoint { get; set; }
|
||||||
public string ChapterTitle { get; set; }
|
public string ChapterTitle { get; set; }
|
||||||
public ChannelWatermark Watermark { get; set; }
|
public List<ChannelWatermark> Watermarks { get; set; }
|
||||||
public int? WatermarkId { get; set; }
|
|
||||||
public bool DisableWatermarks { get; set; }
|
public bool DisableWatermarks { get; set; }
|
||||||
public string PreferredAudioLanguageCode { get; set; }
|
public string PreferredAudioLanguageCode { get; set; }
|
||||||
public string PreferredAudioTitle { get; set; }
|
public string PreferredAudioTitle { get; set; }
|
||||||
@@ -32,6 +31,7 @@ public class PlayoutItem
|
|||||||
public string BlockKey { get; set; }
|
public string BlockKey { get; set; }
|
||||||
public string CollectionKey { get; set; }
|
public string CollectionKey { get; set; }
|
||||||
public string CollectionEtag { get; set; }
|
public string CollectionEtag { get; set; }
|
||||||
|
public List<PlayoutItemWatermark> PlayoutItemWatermarks { get; set; }
|
||||||
public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime();
|
public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime();
|
||||||
public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime();
|
public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime();
|
||||||
|
|
||||||
@@ -55,8 +55,7 @@ public class PlayoutItem
|
|||||||
InPoint = chapter.StartTime,
|
InPoint = chapter.StartTime,
|
||||||
OutPoint = chapter.EndTime,
|
OutPoint = chapter.EndTime,
|
||||||
ChapterTitle = chapter.Title,
|
ChapterTitle = chapter.Title,
|
||||||
Watermark = Watermark,
|
Watermarks = Watermarks,
|
||||||
WatermarkId = WatermarkId,
|
|
||||||
DisableWatermarks = DisableWatermarks,
|
DisableWatermarks = DisableWatermarks,
|
||||||
PreferredAudioLanguageCode = PreferredAudioLanguageCode,
|
PreferredAudioLanguageCode = PreferredAudioLanguageCode,
|
||||||
PreferredAudioTitle = PreferredAudioTitle,
|
PreferredAudioTitle = PreferredAudioTitle,
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
public class PlayoutItemWatermark
|
||||||
|
{
|
||||||
|
public int PlayoutItemId { get; set; }
|
||||||
|
public PlayoutItem PlayoutItem { get; set; }
|
||||||
|
public ChannelWatermark Watermark { get; set; }
|
||||||
|
public int? WatermarkId { get; set; }
|
||||||
|
}
|
||||||
@@ -62,7 +62,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
DateTimeOffset start,
|
DateTimeOffset start,
|
||||||
DateTimeOffset finish,
|
DateTimeOffset finish,
|
||||||
DateTimeOffset now,
|
DateTimeOffset now,
|
||||||
Option<ChannelWatermark> playoutItemWatermark,
|
List<ChannelWatermark> playoutItemWatermarks,
|
||||||
Option<ChannelWatermark> globalWatermark,
|
Option<ChannelWatermark> globalWatermark,
|
||||||
string vaapiDisplay,
|
string vaapiDisplay,
|
||||||
VaapiDriver vaapiDriver,
|
VaapiDriver vaapiDriver,
|
||||||
@@ -157,30 +157,6 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Option<WatermarkOptions> watermarkOptions = disableWatermarks
|
|
||||||
? None
|
|
||||||
: await _ffmpegProcessService.GetWatermarkOptions(
|
|
||||||
ffprobePath,
|
|
||||||
channel,
|
|
||||||
playoutItemWatermark,
|
|
||||||
globalWatermark,
|
|
||||||
videoVersion,
|
|
||||||
None,
|
|
||||||
None);
|
|
||||||
|
|
||||||
Option<List<FadePoint>> maybeFadePoints = watermarkOptions
|
|
||||||
.Map(o => o.Watermark)
|
|
||||||
.Flatten()
|
|
||||||
.Where(wm => wm.Mode == ChannelWatermarkMode.Intermittent)
|
|
||||||
.Map(wm =>
|
|
||||||
WatermarkCalculator.CalculateFadePoints(
|
|
||||||
start,
|
|
||||||
inPoint,
|
|
||||||
outPoint,
|
|
||||||
playbackSettings.StreamSeek,
|
|
||||||
wm.FrequencyMinutes,
|
|
||||||
wm.DurationSeconds));
|
|
||||||
|
|
||||||
string audioFormat = playbackSettings.AudioFormat switch
|
string audioFormat = playbackSettings.AudioFormat switch
|
||||||
{
|
{
|
||||||
FFmpegProfileAudioFormat.Aac => AudioFormat.Aac,
|
FFmpegProfileAudioFormat.Aac => AudioFormat.Aac,
|
||||||
@@ -345,31 +321,67 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
method);
|
method);
|
||||||
}).Flatten();
|
}).Flatten();
|
||||||
|
|
||||||
Option<WatermarkInputFile> watermarkInputFile = GetWatermarkInputFile(watermarkOptions, maybeFadePoints);
|
Option<WatermarkInputFile> watermarkInputFile = Option<WatermarkInputFile>.None;
|
||||||
Option<GraphicsEngineInput> graphicsEngineInput = Option<GraphicsEngineInput>.None;
|
Option<GraphicsEngineInput> graphicsEngineInput = Option<GraphicsEngineInput>.None;
|
||||||
Option<GraphicsEngineContext> graphicsEngineContext = Option<GraphicsEngineContext>.None;
|
Option<GraphicsEngineContext> graphicsEngineContext = Option<GraphicsEngineContext>.None;
|
||||||
|
|
||||||
// use graphics engine for permanent watermarks, or opacity expressions
|
// use graphics engine for all watermarks
|
||||||
var maybeGraphicsEngineWatermark = watermarkOptions
|
if (!disableWatermarks)
|
||||||
.Where(o => o.Watermark
|
|
||||||
.Map(wm => wm.Mode is ChannelWatermarkMode.Permanent or ChannelWatermarkMode.OpacityExpression)
|
|
||||||
.IfNone(false));
|
|
||||||
foreach (var options in maybeGraphicsEngineWatermark)
|
|
||||||
{
|
{
|
||||||
watermarkInputFile = Option<WatermarkInputFile>.None;
|
var watermarks = new Dictionary<int, WatermarkElementContext>();
|
||||||
|
|
||||||
graphicsEngineInput = new GraphicsEngineInput();
|
// still need channel and global watermarks
|
||||||
|
if (playoutItemWatermarks.Count == 0)
|
||||||
|
{
|
||||||
|
WatermarkOptions options = await _ffmpegProcessService.GetWatermarkOptions(
|
||||||
|
ffprobePath,
|
||||||
|
channel,
|
||||||
|
Option<ChannelWatermark>.None,
|
||||||
|
globalWatermark,
|
||||||
|
videoVersion,
|
||||||
|
None,
|
||||||
|
None);
|
||||||
|
|
||||||
WatermarkElementContext watermark = new WatermarkElementContext(options);
|
foreach (var watermark in options.Watermark)
|
||||||
|
{
|
||||||
|
// don't allow duplicates
|
||||||
|
watermarks.TryAdd(watermark.Id, new WatermarkElementContext(options));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
graphicsEngineContext = new GraphicsEngineContext(
|
// load all playout item watermarks
|
||||||
[watermark],
|
foreach (var playoutItemWatermark in playoutItemWatermarks)
|
||||||
channel.FFmpegProfile.Resolution,
|
{
|
||||||
await playbackSettings.FrameRate.IfNoneAsync(24),
|
WatermarkOptions options = await _ffmpegProcessService.GetWatermarkOptions(
|
||||||
ChannelStartTime: channelStartTime,
|
ffprobePath,
|
||||||
ContentStartTime: start,
|
channel,
|
||||||
await playbackSettings.StreamSeek.IfNoneAsync(TimeSpan.Zero),
|
playoutItemWatermark,
|
||||||
finish - now);
|
globalWatermark,
|
||||||
|
videoVersion,
|
||||||
|
None,
|
||||||
|
None);
|
||||||
|
|
||||||
|
foreach (var watermark in options.Watermark)
|
||||||
|
{
|
||||||
|
// don't allow duplicates
|
||||||
|
watermarks.TryAdd(watermark.Id, new WatermarkElementContext(options));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only use graphics engine when we have watermarks
|
||||||
|
if (watermarks.Count > 0)
|
||||||
|
{
|
||||||
|
graphicsEngineInput = new GraphicsEngineInput();
|
||||||
|
|
||||||
|
graphicsEngineContext = new GraphicsEngineContext(
|
||||||
|
watermarks.Values.OfType<GraphicsElementContext>().ToList(),
|
||||||
|
channel.FFmpegProfile.Resolution,
|
||||||
|
await playbackSettings.FrameRate.IfNoneAsync(24),
|
||||||
|
ChannelStartTime: channelStartTime,
|
||||||
|
ContentStartTime: start,
|
||||||
|
await playbackSettings.StreamSeek.IfNoneAsync(TimeSpan.Zero),
|
||||||
|
finish - now);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings, fillerKind);
|
HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings, fillerKind);
|
||||||
@@ -1014,71 +1026,6 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
return GetCommand(ffmpegPath, videoInputFile, None, None, None, None, pipeline, false);
|
return GetCommand(ffmpegPath, videoInputFile, None, None, None, None, pipeline, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Option<WatermarkInputFile> GetWatermarkInputFile(
|
|
||||||
Option<WatermarkOptions> watermarkOptions,
|
|
||||||
Option<List<FadePoint>> maybeFadePoints)
|
|
||||||
{
|
|
||||||
foreach (WatermarkOptions options in watermarkOptions)
|
|
||||||
{
|
|
||||||
foreach (ChannelWatermark watermark in options.Watermark)
|
|
||||||
{
|
|
||||||
// skip watermark if intermittent and no fade points
|
|
||||||
if (watermark.Mode != ChannelWatermarkMode.None &&
|
|
||||||
(watermark.Mode != ChannelWatermarkMode.Intermittent ||
|
|
||||||
maybeFadePoints.Map(fp => fp.Count > 0).IfNone(false)))
|
|
||||||
{
|
|
||||||
foreach (string path in options.ImagePath)
|
|
||||||
{
|
|
||||||
var watermarkInputFile = new WatermarkInputFile(
|
|
||||||
path,
|
|
||||||
new List<VideoStream>
|
|
||||||
{
|
|
||||||
new(
|
|
||||||
options.ImageStreamIndex.IfNone(0),
|
|
||||||
"unknown",
|
|
||||||
string.Empty,
|
|
||||||
new PixelFormatUnknown(),
|
|
||||||
ColorParams.Default,
|
|
||||||
new FrameSize(1, 1),
|
|
||||||
string.Empty,
|
|
||||||
string.Empty,
|
|
||||||
Option<string>.None,
|
|
||||||
!options.IsAnimated,
|
|
||||||
ScanKind.Progressive)
|
|
||||||
},
|
|
||||||
new WatermarkState(
|
|
||||||
maybeFadePoints.Map(lst => lst.Map(fp =>
|
|
||||||
{
|
|
||||||
return fp switch
|
|
||||||
{
|
|
||||||
FadeInPoint fip => (WatermarkFadePoint)new WatermarkFadeIn(
|
|
||||||
fip.Time,
|
|
||||||
fip.EnableStart,
|
|
||||||
fip.EnableFinish),
|
|
||||||
FadeOutPoint fop => new WatermarkFadeOut(
|
|
||||||
fop.Time,
|
|
||||||
fop.EnableStart,
|
|
||||||
fop.EnableFinish),
|
|
||||||
_ => throw new NotSupportedException() // this will never happen
|
|
||||||
};
|
|
||||||
}).ToList()),
|
|
||||||
watermark.Location,
|
|
||||||
watermark.Size,
|
|
||||||
watermark.WidthPercent,
|
|
||||||
watermark.HorizontalMarginPercent,
|
|
||||||
watermark.VerticalMarginPercent,
|
|
||||||
watermark.Opacity,
|
|
||||||
watermark.PlaceWithinSourceContent));
|
|
||||||
|
|
||||||
return watermarkInputFile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Command GetCommand(
|
private Command GetCommand(
|
||||||
string ffmpegPath,
|
string ffmpegPath,
|
||||||
Option<VideoInputFile> videoInputFile,
|
Option<VideoInputFile> videoInputFile,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public interface IFFmpegProcessService
|
|||||||
DateTimeOffset start,
|
DateTimeOffset start,
|
||||||
DateTimeOffset finish,
|
DateTimeOffset finish,
|
||||||
DateTimeOffset now,
|
DateTimeOffset now,
|
||||||
Option<ChannelWatermark> playoutItemWatermark,
|
List<ChannelWatermark> playoutItemWatermarks,
|
||||||
Option<ChannelWatermark> globalWatermark,
|
Option<ChannelWatermark> globalWatermark,
|
||||||
string vaapiDisplay,
|
string vaapiDisplay,
|
||||||
VaapiDriver vaapiDriver,
|
VaapiDriver vaapiDriver,
|
||||||
|
|||||||
@@ -426,10 +426,13 @@ public class PlayoutBuilder : IPlayoutBuilder
|
|||||||
// so only log the warning, and leave the bad data to fail tests
|
// so only log the warning, and leave the bad data to fail tests
|
||||||
// playout.Items.Remove(futureItem);
|
// playout.Items.Remove(futureItem);
|
||||||
|
|
||||||
_logger.LogInformation(
|
if (futureItemCount > 0)
|
||||||
"{Count} playout items are scheduled after hard stop of {HardStop}; this is expected if duration is used.",
|
{
|
||||||
futureItemCount,
|
_logger.LogInformation(
|
||||||
trimAfter);
|
"{Count} playout items are scheduled after hard stop of {HardStop}; this is expected if duration is used.",
|
||||||
|
futureItemCount,
|
||||||
|
trimAfter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return playout;
|
return playout;
|
||||||
|
|||||||
@@ -157,13 +157,18 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
|
|||||||
? FillerKind.GuideMode
|
? FillerKind.GuideMode
|
||||||
: FillerKind.None,
|
: FillerKind.None,
|
||||||
CustomTitle = scheduleItem.CustomTitle,
|
CustomTitle = scheduleItem.CustomTitle,
|
||||||
WatermarkId = scheduleItem.WatermarkId,
|
|
||||||
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||||
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||||
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||||
SubtitleMode = scheduleItem.SubtitleMode
|
SubtitleMode = scheduleItem.SubtitleMode
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (scheduleItem.WatermarkId is not null)
|
||||||
|
{
|
||||||
|
playoutItem.Watermarks ??= [];
|
||||||
|
playoutItem.Watermarks.Add(scheduleItem.Watermark);
|
||||||
|
}
|
||||||
|
|
||||||
durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime);
|
durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime);
|
||||||
|
|
||||||
DateTimeOffset durationFinish = nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc);
|
DateTimeOffset durationFinish = nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc);
|
||||||
|
|||||||
@@ -76,13 +76,18 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
|
|||||||
? FillerKind.GuideMode
|
? FillerKind.GuideMode
|
||||||
: FillerKind.None,
|
: FillerKind.None,
|
||||||
CustomTitle = scheduleItem.CustomTitle,
|
CustomTitle = scheduleItem.CustomTitle,
|
||||||
WatermarkId = scheduleItem.WatermarkId,
|
|
||||||
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||||
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||||
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||||
SubtitleMode = scheduleItem.SubtitleMode
|
SubtitleMode = scheduleItem.SubtitleMode
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (scheduleItem.WatermarkId is not null)
|
||||||
|
{
|
||||||
|
playoutItem.Watermarks ??= [];
|
||||||
|
playoutItem.Watermarks.Add(scheduleItem.Watermark);
|
||||||
|
}
|
||||||
|
|
||||||
var enumeratorStates = new Dictionary<CollectionKey, CollectionEnumeratorState>();
|
var enumeratorStates = new Dictionary<CollectionKey, CollectionEnumeratorState>();
|
||||||
foreach ((CollectionKey key, IMediaCollectionEnumerator enumerator) in collectionEnumerators)
|
foreach ((CollectionKey key, IMediaCollectionEnumerator enumerator) in collectionEnumerators)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -100,13 +100,18 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
|
|||||||
? FillerKind.GuideMode
|
? FillerKind.GuideMode
|
||||||
: FillerKind.None,
|
: FillerKind.None,
|
||||||
CustomTitle = scheduleItem.CustomTitle,
|
CustomTitle = scheduleItem.CustomTitle,
|
||||||
WatermarkId = scheduleItem.WatermarkId,
|
|
||||||
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||||
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||||
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||||
SubtitleMode = scheduleItem.SubtitleMode
|
SubtitleMode = scheduleItem.SubtitleMode
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (scheduleItem.WatermarkId is not null)
|
||||||
|
{
|
||||||
|
playoutItem.Watermarks ??= [];
|
||||||
|
playoutItem.Watermarks.Add(scheduleItem.Watermark);
|
||||||
|
}
|
||||||
|
|
||||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||||
|
|
||||||
playoutItems.AddRange(
|
playoutItems.AddRange(
|
||||||
|
|||||||
@@ -47,13 +47,18 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
|
|||||||
? FillerKind.GuideMode
|
? FillerKind.GuideMode
|
||||||
: FillerKind.None,
|
: FillerKind.None,
|
||||||
CustomTitle = scheduleItem.CustomTitle,
|
CustomTitle = scheduleItem.CustomTitle,
|
||||||
WatermarkId = scheduleItem.WatermarkId,
|
|
||||||
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||||
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||||
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||||
SubtitleMode = scheduleItem.SubtitleMode
|
SubtitleMode = scheduleItem.SubtitleMode
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (scheduleItem.WatermarkId is not null)
|
||||||
|
{
|
||||||
|
playoutItem.Watermarks ??= [];
|
||||||
|
playoutItem.Watermarks.Add(scheduleItem.Watermark);
|
||||||
|
}
|
||||||
|
|
||||||
List<PlayoutItem> playoutItems = AddFiller(
|
List<PlayoutItem> playoutItems = AddFiller(
|
||||||
playoutBuilderState,
|
playoutBuilderState,
|
||||||
collectionEnumerators,
|
collectionEnumerators,
|
||||||
|
|||||||
@@ -58,17 +58,23 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou
|
|||||||
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||||
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||||
//SubtitleMode = scheduleItem.SubtitleMode
|
//SubtitleMode = scheduleItem.SubtitleMode
|
||||||
GuideGroup = context.PeekNextGuideGroup()
|
GuideGroup = context.PeekNextGuideGroup(),
|
||||||
//GuideStart = effectiveBlock.Start.UtcDateTime,
|
//GuideStart = effectiveBlock.Start.UtcDateTime,
|
||||||
//GuideFinish = blockFinish.UtcDateTime,
|
//GuideFinish = blockFinish.UtcDateTime,
|
||||||
//BlockKey = JsonConvert.SerializeObject(effectiveBlock.BlockKey),
|
//BlockKey = JsonConvert.SerializeObject(effectiveBlock.BlockKey),
|
||||||
//CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings),
|
//CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings),
|
||||||
//CollectionEtag = collectionEtags[collectionKey]
|
//CollectionEtag = collectionEtags[collectionKey]
|
||||||
|
PlayoutItemWatermarks = []
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (int watermarkId in context.GetChannelWatermarkId())
|
foreach (int watermarkId in context.GetChannelWatermarkIds())
|
||||||
{
|
{
|
||||||
playoutItem.WatermarkId = watermarkId;
|
playoutItem.PlayoutItemWatermarks.Add(
|
||||||
|
new PlayoutItemWatermark
|
||||||
|
{
|
||||||
|
PlayoutItem = playoutItem,
|
||||||
|
WatermarkId = watermarkId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await AddItemAndMidRoll(context, playoutItem, mediaItem, executeSequence);
|
await AddItemAndMidRoll(context, playoutItem, mediaItem, executeSequence);
|
||||||
|
|||||||
@@ -83,17 +83,23 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay
|
|||||||
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||||
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||||
//SubtitleMode = scheduleItem.SubtitleMode
|
//SubtitleMode = scheduleItem.SubtitleMode
|
||||||
GuideGroup = context.PeekNextGuideGroup()
|
GuideGroup = context.PeekNextGuideGroup(),
|
||||||
//GuideStart = effectiveBlock.Start.UtcDateTime,
|
//GuideStart = effectiveBlock.Start.UtcDateTime,
|
||||||
//GuideFinish = blockFinish.UtcDateTime,
|
//GuideFinish = blockFinish.UtcDateTime,
|
||||||
//BlockKey = JsonConvert.SerializeObject(effectiveBlock.BlockKey),
|
//BlockKey = JsonConvert.SerializeObject(effectiveBlock.BlockKey),
|
||||||
//CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings),
|
//CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings),
|
||||||
//CollectionEtag = collectionEtags[collectionKey]
|
//CollectionEtag = collectionEtags[collectionKey],
|
||||||
|
PlayoutItemWatermarks = []
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (int watermarkId in context.GetChannelWatermarkId())
|
foreach (int watermarkId in context.GetChannelWatermarkIds())
|
||||||
{
|
{
|
||||||
playoutItem.WatermarkId = watermarkId;
|
playoutItem.PlayoutItemWatermarks.Add(
|
||||||
|
new PlayoutItemWatermark
|
||||||
|
{
|
||||||
|
PlayoutItem = playoutItem,
|
||||||
|
WatermarkId = watermarkId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await AddItemAndMidRoll(context, playoutItem, mediaItem, executeSequence);
|
await AddItemAndMidRoll(context, playoutItem, mediaItem, executeSequence);
|
||||||
|
|||||||
@@ -122,12 +122,18 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
|
|||||||
GuideGroup = context.PeekNextGuideGroup(),
|
GuideGroup = context.PeekNextGuideGroup(),
|
||||||
FillerKind = fillerKind,
|
FillerKind = fillerKind,
|
||||||
CustomTitle = string.IsNullOrWhiteSpace(customTitle) ? null : customTitle,
|
CustomTitle = string.IsNullOrWhiteSpace(customTitle) ? null : customTitle,
|
||||||
DisableWatermarks = disableWatermarks
|
DisableWatermarks = disableWatermarks,
|
||||||
|
PlayoutItemWatermarks = []
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (int watermarkId in context.GetChannelWatermarkId())
|
foreach (int watermarkId in context.GetChannelWatermarkIds())
|
||||||
{
|
{
|
||||||
playoutItem.WatermarkId = watermarkId;
|
playoutItem.PlayoutItemWatermarks.Add(
|
||||||
|
new PlayoutItemWatermark
|
||||||
|
{
|
||||||
|
PlayoutItem = playoutItem,
|
||||||
|
WatermarkId = watermarkId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remainingToFill - itemDuration >= TimeSpan.Zero || !stopBeforeEnd)
|
if (remainingToFill - itemDuration >= TimeSpan.Zero || !stopBeforeEnd)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
|
|||||||
|
|
||||||
public class YamlPlayoutWatermarkHandler(IChannelRepository channelRepository) : IYamlPlayoutHandler
|
public class YamlPlayoutWatermarkHandler(IChannelRepository channelRepository) : IYamlPlayoutHandler
|
||||||
{
|
{
|
||||||
|
private readonly Dictionary<string, Option<ChannelWatermark>> _watermarkCache = new();
|
||||||
|
|
||||||
public bool Reset => false;
|
public bool Reset => false;
|
||||||
|
|
||||||
public async Task<bool> Handle(
|
public async Task<bool> Handle(
|
||||||
@@ -24,17 +26,48 @@ public class YamlPlayoutWatermarkHandler(IChannelRepository channelRepository) :
|
|||||||
|
|
||||||
if (watermark.Watermark && !string.IsNullOrWhiteSpace(watermark.Name))
|
if (watermark.Watermark && !string.IsNullOrWhiteSpace(watermark.Name))
|
||||||
{
|
{
|
||||||
Option<ChannelWatermark> maybeWatermark = await channelRepository.GetWatermarkByName(watermark.Name);
|
foreach (var wm in await GetChannelWatermarkByName(watermark.Name))
|
||||||
foreach (ChannelWatermark channelWatermark in maybeWatermark)
|
|
||||||
{
|
{
|
||||||
context.SetChannelWatermarkId(channelWatermark.Id);
|
context.SetChannelWatermarkId(wm.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
context.ClearChannelWatermarkId();
|
if (!string.IsNullOrWhiteSpace(watermark.Name))
|
||||||
|
{
|
||||||
|
foreach (var wm in await GetChannelWatermarkByName(watermark.Name))
|
||||||
|
{
|
||||||
|
context.RemoveChannelWatermarkId(wm.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.ClearChannelWatermarkIds();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<Option<ChannelWatermark>> GetChannelWatermarkByName(string name)
|
||||||
|
{
|
||||||
|
if (_watermarkCache.TryGetValue(name, out var cachedWatermark))
|
||||||
|
{
|
||||||
|
foreach (ChannelWatermark channelWatermark in cachedWatermark)
|
||||||
|
{
|
||||||
|
return channelWatermark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Option<ChannelWatermark> maybeWatermark = await channelRepository.GetWatermarkByName(name);
|
||||||
|
_watermarkCache.Add(name, maybeWatermark);
|
||||||
|
foreach (ChannelWatermark channelWatermark in maybeWatermark)
|
||||||
|
{
|
||||||
|
return channelWatermark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Option<ChannelWatermark>.None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
|
|||||||
|
|
||||||
private readonly System.Collections.Generic.HashSet<int> _visitedInstructions = [];
|
private readonly System.Collections.Generic.HashSet<int> _visitedInstructions = [];
|
||||||
private readonly Stack<FillerKind> _fillerKind = new();
|
private readonly Stack<FillerKind> _fillerKind = new();
|
||||||
|
private readonly System.Collections.Generic.HashSet<int> _channelWatermarkIds = [];
|
||||||
private int _guideGroup = guideGroup;
|
private int _guideGroup = guideGroup;
|
||||||
private bool _guideGroupLocked;
|
private bool _guideGroupLocked;
|
||||||
private int _instructionIndex;
|
private int _instructionIndex;
|
||||||
private Option<int> _channelWatermarkId;
|
|
||||||
private Option<string> _preRollSequence;
|
private Option<string> _preRollSequence;
|
||||||
private Option<string> _postRollSequence;
|
private Option<string> _postRollSequence;
|
||||||
private Option<MidRollSequence> _midRollSequence;
|
private Option<MidRollSequence> _midRollSequence;
|
||||||
@@ -84,15 +84,20 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
|
|||||||
|
|
||||||
public void SetChannelWatermarkId(int id)
|
public void SetChannelWatermarkId(int id)
|
||||||
{
|
{
|
||||||
_channelWatermarkId = id;
|
_channelWatermarkIds.Add(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearChannelWatermarkId()
|
public void RemoveChannelWatermarkId(int id)
|
||||||
{
|
{
|
||||||
_channelWatermarkId = Option<int>.None;
|
_channelWatermarkIds.Remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Option<int> GetChannelWatermarkId() => _channelWatermarkId;
|
public void ClearChannelWatermarkIds()
|
||||||
|
{
|
||||||
|
_channelWatermarkIds.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<int> GetChannelWatermarkIds() => _channelWatermarkIds.ToList();
|
||||||
|
|
||||||
public void SetPreRollSequence(string sequence)
|
public void SetPreRollSequence(string sequence)
|
||||||
{
|
{
|
||||||
@@ -142,19 +147,19 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
|
|||||||
|
|
||||||
public string Serialize()
|
public string Serialize()
|
||||||
{
|
{
|
||||||
int? channelWatermarkId = null;
|
|
||||||
foreach (int id in _channelWatermarkId)
|
|
||||||
{
|
|
||||||
channelWatermarkId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
string preRollSequence = null;
|
string preRollSequence = null;
|
||||||
foreach (string sequence in _preRollSequence)
|
foreach (string sequence in _preRollSequence)
|
||||||
{
|
{
|
||||||
preRollSequence = sequence;
|
preRollSequence = sequence;
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = new State(_instructionIndex, _guideGroup, _guideGroupLocked, channelWatermarkId, preRollSequence);
|
var state = new State(
|
||||||
|
_instructionIndex,
|
||||||
|
_guideGroup,
|
||||||
|
_guideGroupLocked,
|
||||||
|
_channelWatermarkIds.ToList(),
|
||||||
|
preRollSequence);
|
||||||
|
|
||||||
return JsonConvert.SerializeObject(state, Formatting.None, JsonSettings);
|
return JsonConvert.SerializeObject(state, Formatting.None, JsonSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,9 +189,9 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
|
|||||||
_guideGroupLocked = guideGroupLocked;
|
_guideGroupLocked = guideGroupLocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (int channelWatermarkId in Optional(state.ChannelWatermarkId))
|
foreach (int channelWatermarkId in state.ChannelWatermarkIds)
|
||||||
{
|
{
|
||||||
_channelWatermarkId = channelWatermarkId;
|
_channelWatermarkIds.Add(channelWatermarkId);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (string preRollSequence in Optional(state.PreRollSequence))
|
foreach (string preRollSequence in Optional(state.PreRollSequence))
|
||||||
@@ -199,7 +204,7 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
|
|||||||
int? InstructionIndex,
|
int? InstructionIndex,
|
||||||
int? GuideGroup,
|
int? GuideGroup,
|
||||||
bool? GuideGroupLocked,
|
bool? GuideGroupLocked,
|
||||||
int? ChannelWatermarkId,
|
List<int> ChannelWatermarkIds,
|
||||||
string PreRollSequence);
|
string PreRollSequence);
|
||||||
|
|
||||||
public record MidRollSequence(string Sequence, string Expression);
|
public record MidRollSequence(string Sequence, string Expression);
|
||||||
|
|||||||
Generated
+6223
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Add_PlayoutItemWatermark : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PlayoutItemWatermark",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
PlayoutItemId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
WatermarkId = table.Column<int>(type: "int", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PlayoutItemWatermark", x => new { x.PlayoutItemId, x.WatermarkId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PlayoutItemWatermark_ChannelWatermark_WatermarkId",
|
||||||
|
column: x => x.WatermarkId,
|
||||||
|
principalTable: "ChannelWatermark",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PlayoutItemWatermark_PlayoutItem_PlayoutItemId",
|
||||||
|
column: x => x.PlayoutItemId,
|
||||||
|
principalTable: "PlayoutItem",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PlayoutItemWatermark_WatermarkId",
|
||||||
|
table: "PlayoutItemWatermark",
|
||||||
|
column: "WatermarkId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PlayoutItemWatermark");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+6223
File diff suppressed because it is too large
Load Diff
+23
@@ -0,0 +1,23 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Populate_PlayoutItemWatermark : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
@"INSERT INTO `PlayoutItemWatermark` (`PlayoutItemId`, `WatermarkId`)
|
||||||
|
SELECT `Id`, `WatermarkId` FROM `PlayoutItem` WHERE `WatermarkId` IS NOT NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+6211
File diff suppressed because it is too large
Load Diff
+49
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Remove_PlayoutItemWatermark : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_PlayoutItem_ChannelWatermark_WatermarkId",
|
||||||
|
table: "PlayoutItem");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_PlayoutItem_WatermarkId",
|
||||||
|
table: "PlayoutItem");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WatermarkId",
|
||||||
|
table: "PlayoutItem");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "WatermarkId",
|
||||||
|
table: "PlayoutItem",
|
||||||
|
type: "int",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PlayoutItem_WatermarkId",
|
||||||
|
table: "PlayoutItem",
|
||||||
|
column: "WatermarkId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_PlayoutItem_ChannelWatermark_WatermarkId",
|
||||||
|
table: "PlayoutItem",
|
||||||
|
column: "WatermarkId",
|
||||||
|
principalTable: "ChannelWatermark",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1882,18 +1882,28 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
|||||||
b.Property<int?>("SubtitleMode")
|
b.Property<int?>("SubtitleMode")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int?>("WatermarkId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("MediaItemId");
|
b.HasIndex("MediaItemId");
|
||||||
|
|
||||||
b.HasIndex("PlayoutId");
|
b.HasIndex("PlayoutId");
|
||||||
|
|
||||||
|
b.ToTable("PlayoutItem", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItemWatermark", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("PlayoutItemId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("WatermarkId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("PlayoutItemId", "WatermarkId");
|
||||||
|
|
||||||
b.HasIndex("WatermarkId");
|
b.HasIndex("WatermarkId");
|
||||||
|
|
||||||
b.ToTable("PlayoutItem", (string)null);
|
b.ToTable("PlayoutItemWatermark");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b =>
|
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b =>
|
||||||
@@ -4617,14 +4627,26 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("WatermarkId")
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
b.Navigation("MediaItem");
|
b.Navigation("MediaItem");
|
||||||
|
|
||||||
b.Navigation("Playout");
|
b.Navigation("Playout");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItemWatermark", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ErsatzTV.Core.Domain.PlayoutItem", "PlayoutItem")
|
||||||
|
.WithMany("PlayoutItemWatermarks")
|
||||||
|
.HasForeignKey("PlayoutItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
|
||||||
|
.WithMany("PlayoutItemWatermarks")
|
||||||
|
.HasForeignKey("WatermarkId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("PlayoutItem");
|
||||||
|
|
||||||
b.Navigation("Watermark");
|
b.Navigation("Watermark");
|
||||||
});
|
});
|
||||||
@@ -5748,6 +5770,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
|||||||
b.Navigation("Playouts");
|
b.Navigation("Playouts");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelWatermark", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("PlayoutItemWatermarks");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b =>
|
modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("CollectionItems");
|
b.Navigation("CollectionItems");
|
||||||
@@ -5930,6 +5957,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
|||||||
b.Navigation("Templates");
|
b.Navigation("Templates");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("PlayoutItemWatermarks");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
|
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Items");
|
b.Navigation("Items");
|
||||||
|
|||||||
Generated
+6060
File diff suppressed because it is too large
Load Diff
+50
@@ -0,0 +1,50 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Add_PlayoutItemWatermarkTable : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PlayoutItemWatermark",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
PlayoutItemId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
WatermarkId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PlayoutItemWatermark", x => new { x.PlayoutItemId, x.WatermarkId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PlayoutItemWatermark_ChannelWatermark_WatermarkId",
|
||||||
|
column: x => x.WatermarkId,
|
||||||
|
principalTable: "ChannelWatermark",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PlayoutItemWatermark_PlayoutItem_PlayoutItemId",
|
||||||
|
column: x => x.PlayoutItemId,
|
||||||
|
principalTable: "PlayoutItem",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PlayoutItemWatermark_WatermarkId",
|
||||||
|
table: "PlayoutItemWatermark",
|
||||||
|
column: "WatermarkId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PlayoutItemWatermark");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+6060
File diff suppressed because it is too large
Load Diff
+23
@@ -0,0 +1,23 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Populate_PlayoutItemWatermark : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
@"INSERT INTO `PlayoutItemWatermark` (`PlayoutItemId`, `WatermarkId`)
|
||||||
|
SELECT `Id`, `WatermarkId` FROM `PlayoutItem` WHERE `WatermarkId` IS NOT NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+6048
File diff suppressed because it is too large
Load Diff
+49
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Remove_PlayoutItemWatermark : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_PlayoutItem_ChannelWatermark_WatermarkId",
|
||||||
|
table: "PlayoutItem");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_PlayoutItem_WatermarkId",
|
||||||
|
table: "PlayoutItem");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WatermarkId",
|
||||||
|
table: "PlayoutItem");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "WatermarkId",
|
||||||
|
table: "PlayoutItem",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PlayoutItem_WatermarkId",
|
||||||
|
table: "PlayoutItem",
|
||||||
|
column: "WatermarkId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_PlayoutItem_ChannelWatermark_WatermarkId",
|
||||||
|
table: "PlayoutItem",
|
||||||
|
column: "WatermarkId",
|
||||||
|
principalTable: "ChannelWatermark",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1793,18 +1793,28 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
|||||||
b.Property<int?>("SubtitleMode")
|
b.Property<int?>("SubtitleMode")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int?>("WatermarkId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("MediaItemId");
|
b.HasIndex("MediaItemId");
|
||||||
|
|
||||||
b.HasIndex("PlayoutId");
|
b.HasIndex("PlayoutId");
|
||||||
|
|
||||||
|
b.ToTable("PlayoutItem", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItemWatermark", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("PlayoutItemId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("WatermarkId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("PlayoutItemId", "WatermarkId");
|
||||||
|
|
||||||
b.HasIndex("WatermarkId");
|
b.HasIndex("WatermarkId");
|
||||||
|
|
||||||
b.ToTable("PlayoutItem", (string)null);
|
b.ToTable("PlayoutItemWatermark");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b =>
|
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b =>
|
||||||
@@ -4454,14 +4464,26 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("WatermarkId")
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
b.Navigation("MediaItem");
|
b.Navigation("MediaItem");
|
||||||
|
|
||||||
b.Navigation("Playout");
|
b.Navigation("Playout");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItemWatermark", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ErsatzTV.Core.Domain.PlayoutItem", "PlayoutItem")
|
||||||
|
.WithMany("PlayoutItemWatermarks")
|
||||||
|
.HasForeignKey("PlayoutItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
|
||||||
|
.WithMany("PlayoutItemWatermarks")
|
||||||
|
.HasForeignKey("WatermarkId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("PlayoutItem");
|
||||||
|
|
||||||
b.Navigation("Watermark");
|
b.Navigation("Watermark");
|
||||||
});
|
});
|
||||||
@@ -5585,6 +5607,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
|||||||
b.Navigation("Playouts");
|
b.Navigation("Playouts");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelWatermark", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("PlayoutItemWatermarks");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b =>
|
modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("CollectionItems");
|
b.Navigation("CollectionItems");
|
||||||
@@ -5767,6 +5794,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
|||||||
b.Navigation("Templates");
|
b.Navigation("Templates");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("PlayoutItemWatermarks");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
|
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Items");
|
b.Navigation("Items");
|
||||||
|
|||||||
@@ -16,10 +16,17 @@ public class PlayoutItemConfiguration : IEntityTypeConfiguration<PlayoutItem>
|
|||||||
.HasForeignKey(pi => pi.MediaItemId)
|
.HasForeignKey(pi => pi.MediaItemId)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
builder.HasOne(i => i.Watermark)
|
builder.HasMany(c => c.Watermarks)
|
||||||
.WithMany()
|
.WithMany(m => m.PlayoutItems)
|
||||||
.HasForeignKey(i => i.WatermarkId)
|
.UsingEntity<PlayoutItemWatermark>(
|
||||||
.OnDelete(DeleteBehavior.SetNull)
|
j => j.HasOne(ci => ci.Watermark)
|
||||||
.IsRequired(false);
|
.WithMany(mi => mi.PlayoutItemWatermarks)
|
||||||
|
.HasForeignKey(ci => ci.WatermarkId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade),
|
||||||
|
j => j.HasOne(ci => ci.PlayoutItem)
|
||||||
|
.WithMany(c => c.PlayoutItemWatermarks)
|
||||||
|
.HasForeignKey(ci => ci.PlayoutItemId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade),
|
||||||
|
j => j.HasKey(ci => new { ci.PlayoutItemId, ci.WatermarkId }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ public class GraphicsEngine(ILogger<GraphicsEngine> logger) : IGraphicsEngine
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
await pipeWriter.CompleteAsync();
|
||||||
|
|
||||||
foreach (var element in elements.OfType<IDisposable>())
|
foreach (var element in elements.OfType<IDisposable>())
|
||||||
{
|
{
|
||||||
element.Dispose();
|
element.Dispose();
|
||||||
|
|||||||
@@ -43,7 +43,22 @@ public class WatermarkElement : IGraphicsElement, IDisposable
|
|||||||
|
|
||||||
public async Task InitializeAsync(Resolution frameSize, int frameRate, CancellationToken cancellationToken)
|
public async Task InitializeAsync(Resolution frameSize, int frameRate, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (_watermark.Mode is ChannelWatermarkMode.OpacityExpression && !string.IsNullOrWhiteSpace(_watermark.OpacityExpression))
|
if (_watermark.Mode is ChannelWatermarkMode.Intermittent)
|
||||||
|
{
|
||||||
|
string expressionString = $@"
|
||||||
|
if(time_of_day_seconds % {_watermark.FrequencyMinutes * 60} < 1,
|
||||||
|
(time_of_day_seconds % {_watermark.FrequencyMinutes * 60}),
|
||||||
|
if(time_of_day_seconds % {_watermark.FrequencyMinutes * 60} < {1 + _watermark.DurationSeconds},
|
||||||
|
1,
|
||||||
|
if(time_of_day_seconds % {_watermark.FrequencyMinutes * 60} < {1 + _watermark.DurationSeconds + 1},
|
||||||
|
1 - ((time_of_day_seconds % {_watermark.FrequencyMinutes * 60} - {1 + _watermark.DurationSeconds}) / 1),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)";
|
||||||
|
_expression = new Expression(expressionString);
|
||||||
|
}
|
||||||
|
else if (_watermark.Mode is ChannelWatermarkMode.OpacityExpression && !string.IsNullOrWhiteSpace(_watermark.OpacityExpression))
|
||||||
{
|
{
|
||||||
_expression = new Expression(_watermark.OpacityExpression);
|
_expression = new Expression(_watermark.OpacityExpression);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ public class TranscodingTests
|
|||||||
now,
|
now,
|
||||||
now + TimeSpan.FromSeconds(3),
|
now + TimeSpan.FromSeconds(3),
|
||||||
now,
|
now,
|
||||||
Option<ChannelWatermark>.None,
|
[],
|
||||||
GetWatermark(watermark),
|
GetWatermark(watermark),
|
||||||
"drm",
|
"drm",
|
||||||
VaapiDriver.RadeonSI,
|
VaapiDriver.RadeonSI,
|
||||||
@@ -646,7 +646,7 @@ public class TranscodingTests
|
|||||||
now,
|
now,
|
||||||
now + TimeSpan.FromSeconds(3),
|
now + TimeSpan.FromSeconds(3),
|
||||||
now,
|
now,
|
||||||
Option<ChannelWatermark>.None,
|
[],
|
||||||
channelWatermark,
|
channelWatermark,
|
||||||
"drm",
|
"drm",
|
||||||
VaapiDriver.RadeonSI,
|
VaapiDriver.RadeonSI,
|
||||||
|
|||||||
@@ -97,12 +97,20 @@
|
|||||||
if (_exists)
|
if (_exists)
|
||||||
{
|
{
|
||||||
_yamlText = await File.ReadAllTextAsync(_yamlFile);
|
_yamlText = await File.ReadAllTextAsync(_yamlFile);
|
||||||
_jsonText = YamlScheduleValidator.ToJson(_yamlText);
|
try
|
||||||
var messages = await YamlScheduleValidator.GetValidationMessages(_yamlText, _isImport);
|
{
|
||||||
_messagesCount = messages.Count;
|
_jsonText = YamlScheduleValidator.ToJson(_yamlText);
|
||||||
_messages = string.Join("\n", messages);
|
var messages = await YamlScheduleValidator.GetValidationMessages(_yamlText, _isImport);
|
||||||
|
_messagesCount = messages.Count;
|
||||||
|
_messages = string.Join("\n", messages);
|
||||||
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_messages = e.ToString();
|
||||||
|
_messagesCount = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -180,7 +180,8 @@
|
|||||||
"all": { "type": "null" },
|
"all": { "type": "null" },
|
||||||
"content": { "type": "string" },
|
"content": { "type": "string" },
|
||||||
"custom_title": { "type": "string" },
|
"custom_title": { "type": "string" },
|
||||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" }
|
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||||
|
"disable_watermarks": { "type": "boolean" }
|
||||||
},
|
},
|
||||||
"required": [ "all", "content" ],
|
"required": [ "all", "content" ],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -191,7 +192,8 @@
|
|||||||
"count": { "type": "integer" },
|
"count": { "type": "integer" },
|
||||||
"content": { "type": "string" },
|
"content": { "type": "string" },
|
||||||
"custom_title": { "type": "string" },
|
"custom_title": { "type": "string" },
|
||||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" }
|
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||||
|
"disable_watermarks": { "type": "boolean" }
|
||||||
},
|
},
|
||||||
"required": [ "count", "content" ],
|
"required": [ "count", "content" ],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -207,7 +209,8 @@
|
|||||||
"offline_tail": { "type": "boolean" },
|
"offline_tail": { "type": "boolean" },
|
||||||
"stop_before_end": { "type": "boolean" },
|
"stop_before_end": { "type": "boolean" },
|
||||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||||
"custom_title": { "type": "string" }
|
"custom_title": { "type": "string" },
|
||||||
|
"disable_watermarks": { "type": "boolean" }
|
||||||
},
|
},
|
||||||
"required": [ "duration", "content" ],
|
"required": [ "duration", "content" ],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -221,7 +224,8 @@
|
|||||||
"fallback": { "type": "string" },
|
"fallback": { "type": "string" },
|
||||||
"discard_attempts": { "type": "integer" },
|
"discard_attempts": { "type": "integer" },
|
||||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||||
"custom_title": { "type": "string" }
|
"custom_title": { "type": "string" },
|
||||||
|
"disable_watermarks": { "type": "boolean" }
|
||||||
},
|
},
|
||||||
"required": [ "pad_to_next", "content" ],
|
"required": [ "pad_to_next", "content" ],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -238,7 +242,8 @@
|
|||||||
"discard_attempts": { "type": "integer" },
|
"discard_attempts": { "type": "integer" },
|
||||||
"stop_before_end": { "type": "boolean" },
|
"stop_before_end": { "type": "boolean" },
|
||||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||||
"custom_title": { "type": "string" }
|
"custom_title": { "type": "string" },
|
||||||
|
"disable_watermarks": { "type": "boolean" }
|
||||||
},
|
},
|
||||||
"required": [ "pad_until", "content" ],
|
"required": [ "pad_until", "content" ],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
@@ -229,7 +229,8 @@
|
|||||||
"all": { "type": "null" },
|
"all": { "type": "null" },
|
||||||
"content": { "type": "string" },
|
"content": { "type": "string" },
|
||||||
"custom_title": { "type": "string" },
|
"custom_title": { "type": "string" },
|
||||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" }
|
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||||
|
"disable_watermarks": { "type": "boolean" }
|
||||||
},
|
},
|
||||||
"required": [ "all", "content" ],
|
"required": [ "all", "content" ],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -240,7 +241,8 @@
|
|||||||
"count": { "type": "integer" },
|
"count": { "type": "integer" },
|
||||||
"content": { "type": "string" },
|
"content": { "type": "string" },
|
||||||
"custom_title": { "type": "string" },
|
"custom_title": { "type": "string" },
|
||||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" }
|
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||||
|
"disable_watermarks": { "type": "boolean" }
|
||||||
},
|
},
|
||||||
"required": [ "count", "content" ],
|
"required": [ "count", "content" ],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -256,7 +258,8 @@
|
|||||||
"offline_tail": { "type": "boolean" },
|
"offline_tail": { "type": "boolean" },
|
||||||
"stop_before_end": { "type": "boolean" },
|
"stop_before_end": { "type": "boolean" },
|
||||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||||
"custom_title": { "type": "string" }
|
"custom_title": { "type": "string" },
|
||||||
|
"disable_watermarks": { "type": "boolean" }
|
||||||
},
|
},
|
||||||
"required": [ "duration", "content" ],
|
"required": [ "duration", "content" ],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -270,7 +273,8 @@
|
|||||||
"fallback": { "type": "string" },
|
"fallback": { "type": "string" },
|
||||||
"discard_attempts": { "type": "integer" },
|
"discard_attempts": { "type": "integer" },
|
||||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||||
"custom_title": { "type": "string" }
|
"custom_title": { "type": "string" },
|
||||||
|
"disable_watermarks": { "type": "boolean" }
|
||||||
},
|
},
|
||||||
"required": [ "pad_to_next", "content" ],
|
"required": [ "pad_to_next", "content" ],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -287,7 +291,8 @@
|
|||||||
"discard_attempts": { "type": "integer" },
|
"discard_attempts": { "type": "integer" },
|
||||||
"stop_before_end": { "type": "boolean" },
|
"stop_before_end": { "type": "boolean" },
|
||||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||||
"custom_title": { "type": "string" }
|
"custom_title": { "type": "string" },
|
||||||
|
"disable_watermarks": { "type": "boolean" }
|
||||||
},
|
},
|
||||||
"required": [ "pad_until", "content" ],
|
"required": [ "pad_until", "content" ],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
Reference in New Issue
Block a user