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]
|
||||
### Added
|
||||
- Add *experimental* graphics engine
|
||||
- `Permanent` watermarks will use new graphics engine
|
||||
- `Opacity Expression` watermarks will use new graphics engine
|
||||
- `Intermittent` watermarks will still use normal overlay pipeline (for now)
|
||||
- All watermarks will use new graphics engine
|
||||
- Add `Opacity Expression` watermark mode
|
||||
- This allows specifying an expression that returns an opacity between 0.0 and 1.0
|
||||
- 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
|
||||
- `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
|
||||
### Added
|
||||
- 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.Deco)
|
||||
.Include(p => p.Items)
|
||||
.ThenInclude(pi => pi.Watermarks)
|
||||
.Include(p => p.PlayoutHistory)
|
||||
.Include(p => p.Templates)
|
||||
.ThenInclude(t => t.Template)
|
||||
@@ -181,6 +182,10 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
.Include(p => p.ProgramScheduleAlternates)
|
||||
.ThenInclude(a => a.ProgramSchedule)
|
||||
.ThenInclude(ps => ps.Items)
|
||||
.ThenInclude(psi => psi.Watermark)
|
||||
.Include(p => p.ProgramScheduleAlternates)
|
||||
.ThenInclude(a => a.ProgramSchedule)
|
||||
.ThenInclude(ps => ps.Items)
|
||||
.ThenInclude(psi => psi.Collection)
|
||||
.Include(p => p.ProgramScheduleAlternates)
|
||||
.ThenInclude(a => a.ProgramSchedule)
|
||||
@@ -212,6 +217,9 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
.ThenInclude(a => a.MediaItem)
|
||||
.Include(p => p.ProgramSchedule)
|
||||
.ThenInclude(ps => ps.Items)
|
||||
.ThenInclude(psi => psi.Watermark)
|
||||
.Include(p => p.ProgramSchedule)
|
||||
.ThenInclude(ps => ps.Items)
|
||||
.ThenInclude(psi => psi.Collection)
|
||||
.Include(p => p.ProgramSchedule)
|
||||
.ThenInclude(ps => ps.Items)
|
||||
|
||||
@@ -467,6 +467,7 @@ public class HlsSessionWorker : IHlsSessionWorker
|
||||
foreach (var graphicsEngineContext in processModel.GraphicsEngineContext)
|
||||
{
|
||||
var pipe = new Pipe();
|
||||
maybePipe = pipe;
|
||||
processWithPipe = process.WithStandardInputPipe(PipeSource.FromStream(pipe.Reader.AsStream()));
|
||||
|
||||
// fire and forget graphics engine task
|
||||
|
||||
+12
-8
@@ -164,7 +164,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
.ThenInclude(mv => mv.Streams)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as Image).ImageMetadata)
|
||||
.Include(i => i.Watermark)
|
||||
.Include(i => i.Watermarks)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as RemoteStream).MediaVersions)
|
||||
.ThenInclude(mv => mv.MediaFiles)
|
||||
@@ -173,7 +173,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
.ThenInclude(mv => mv.Streams)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as RemoteStream).RemoteStreamMetadata)
|
||||
.Include(i => i.Watermark)
|
||||
.Include(i => i.Watermarks)
|
||||
.ForChannelAndTime(channel.Id, now)
|
||||
.Map(o => o.ToEither<BaseError>(new UnableToLocatePlayoutItem()))
|
||||
.BindT(item => ValidatePlayoutItemPath(dbContext, item));
|
||||
@@ -242,7 +242,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
.BindT(watermarkId => dbContext.ChannelWatermarks
|
||||
.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;
|
||||
WatermarkResult watermarkResult = GetPlayoutItemWatermark(playoutItemWithPath.PlayoutItem, now);
|
||||
switch (watermarkResult)
|
||||
@@ -254,7 +256,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
disableWatermarks = true;
|
||||
break;
|
||||
case CustomWatermark watermark:
|
||||
playoutItemWatermark = watermark.Watermark;
|
||||
playoutItemWatermarks.Clear();
|
||||
playoutItemWatermarks.Add(watermark.Watermark);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -263,7 +266,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
(videoPath, videoVersion) = await _songVideoGenerator.GenerateSongVideo(
|
||||
song,
|
||||
channel,
|
||||
playoutItemWatermark,
|
||||
playoutItemWatermarks.HeadOrNone(),
|
||||
maybeGlobalWatermark,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
@@ -278,7 +281,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
string image = is43 ? "song_progress_overlay_43.png" : "song_progress_overlay.png";
|
||||
|
||||
disableWatermarks = false;
|
||||
playoutItemWatermark = new ChannelWatermark
|
||||
playoutItemWatermarks.Clear();
|
||||
playoutItemWatermarks.Add(new ChannelWatermark
|
||||
{
|
||||
Mode = ChannelWatermarkMode.Permanent,
|
||||
Size = WatermarkSize.Scaled,
|
||||
@@ -289,7 +293,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
Location = WatermarkLocation.TopLeft,
|
||||
ImageSource = ChannelWatermarkImageSource.Resource,
|
||||
Image = image
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,7 +339,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
start,
|
||||
finish,
|
||||
effectiveNow,
|
||||
playoutItemWatermark,
|
||||
playoutItemWatermarks,
|
||||
maybeGlobalWatermark,
|
||||
channel.FFmpegProfile.VaapiDisplay,
|
||||
channel.FFmpegProfile.VaapiDriver,
|
||||
|
||||
+4
-4
@@ -78,11 +78,11 @@ public class PrepareTroubleshootingPlaybackHandler(
|
||||
return BaseError.New("Media item does not exist on disk");
|
||||
}
|
||||
|
||||
Option<ChannelWatermark> maybeWatermark = Option<ChannelWatermark>.None;
|
||||
List<ChannelWatermark> watermarks = [];
|
||||
if (request.WatermarkId > 0)
|
||||
{
|
||||
maybeWatermark = await dbContext.ChannelWatermarks
|
||||
.SelectOneAsync(cw => cw.Id, cw => cw.Id == request.WatermarkId);
|
||||
watermarks.AddRange(await dbContext.ChannelWatermarks
|
||||
.SelectOneAsync(cw => cw.Id, cw => cw.Id == request.WatermarkId));
|
||||
}
|
||||
|
||||
DateTimeOffset now = DateTimeOffset.Now;
|
||||
@@ -132,7 +132,7 @@ public class PrepareTroubleshootingPlaybackHandler(
|
||||
now,
|
||||
now + duration,
|
||||
now,
|
||||
maybeWatermark,
|
||||
watermarks,
|
||||
Option<ChannelWatermark>.None,
|
||||
ffmpegProfile.VaapiDisplay,
|
||||
ffmpegProfile.VaapiDriver,
|
||||
|
||||
@@ -20,6 +20,8 @@ public class ChannelWatermark
|
||||
public int Opacity { get; set; }
|
||||
public bool PlaceWithinSourceContent { get; set; }
|
||||
public string OpacityExpression { get; set; }
|
||||
public List<PlayoutItem> PlayoutItems { get; set; }
|
||||
public List<PlayoutItemWatermark> PlayoutItemWatermarks { get; set; }
|
||||
}
|
||||
|
||||
public enum ChannelWatermarkMode
|
||||
|
||||
@@ -22,8 +22,7 @@ public class PlayoutItem
|
||||
public TimeSpan InPoint { get; set; }
|
||||
public TimeSpan OutPoint { get; set; }
|
||||
public string ChapterTitle { get; set; }
|
||||
public ChannelWatermark Watermark { get; set; }
|
||||
public int? WatermarkId { get; set; }
|
||||
public List<ChannelWatermark> Watermarks { get; set; }
|
||||
public bool DisableWatermarks { get; set; }
|
||||
public string PreferredAudioLanguageCode { get; set; }
|
||||
public string PreferredAudioTitle { get; set; }
|
||||
@@ -32,6 +31,7 @@ public class PlayoutItem
|
||||
public string BlockKey { get; set; }
|
||||
public string CollectionKey { get; set; }
|
||||
public string CollectionEtag { get; set; }
|
||||
public List<PlayoutItemWatermark> PlayoutItemWatermarks { get; set; }
|
||||
public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime();
|
||||
public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime();
|
||||
|
||||
@@ -55,8 +55,7 @@ public class PlayoutItem
|
||||
InPoint = chapter.StartTime,
|
||||
OutPoint = chapter.EndTime,
|
||||
ChapterTitle = chapter.Title,
|
||||
Watermark = Watermark,
|
||||
WatermarkId = WatermarkId,
|
||||
Watermarks = Watermarks,
|
||||
DisableWatermarks = DisableWatermarks,
|
||||
PreferredAudioLanguageCode = PreferredAudioLanguageCode,
|
||||
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 finish,
|
||||
DateTimeOffset now,
|
||||
Option<ChannelWatermark> playoutItemWatermark,
|
||||
List<ChannelWatermark> playoutItemWatermarks,
|
||||
Option<ChannelWatermark> globalWatermark,
|
||||
string vaapiDisplay,
|
||||
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
|
||||
{
|
||||
FFmpegProfileAudioFormat.Aac => AudioFormat.Aac,
|
||||
@@ -345,31 +321,67 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
method);
|
||||
}).Flatten();
|
||||
|
||||
Option<WatermarkInputFile> watermarkInputFile = GetWatermarkInputFile(watermarkOptions, maybeFadePoints);
|
||||
Option<WatermarkInputFile> watermarkInputFile = Option<WatermarkInputFile>.None;
|
||||
Option<GraphicsEngineInput> graphicsEngineInput = Option<GraphicsEngineInput>.None;
|
||||
Option<GraphicsEngineContext> graphicsEngineContext = Option<GraphicsEngineContext>.None;
|
||||
|
||||
// use graphics engine for permanent watermarks, or opacity expressions
|
||||
var maybeGraphicsEngineWatermark = watermarkOptions
|
||||
.Where(o => o.Watermark
|
||||
.Map(wm => wm.Mode is ChannelWatermarkMode.Permanent or ChannelWatermarkMode.OpacityExpression)
|
||||
.IfNone(false));
|
||||
foreach (var options in maybeGraphicsEngineWatermark)
|
||||
// use graphics engine for all watermarks
|
||||
if (!disableWatermarks)
|
||||
{
|
||||
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(
|
||||
[watermark],
|
||||
channel.FFmpegProfile.Resolution,
|
||||
await playbackSettings.FrameRate.IfNoneAsync(24),
|
||||
ChannelStartTime: channelStartTime,
|
||||
ContentStartTime: start,
|
||||
await playbackSettings.StreamSeek.IfNoneAsync(TimeSpan.Zero),
|
||||
finish - now);
|
||||
// load all playout item watermarks
|
||||
foreach (var playoutItemWatermark in playoutItemWatermarks)
|
||||
{
|
||||
WatermarkOptions options = await _ffmpegProcessService.GetWatermarkOptions(
|
||||
ffprobePath,
|
||||
channel,
|
||||
playoutItemWatermark,
|
||||
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);
|
||||
@@ -1014,71 +1026,6 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
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(
|
||||
string ffmpegPath,
|
||||
Option<VideoInputFile> videoInputFile,
|
||||
|
||||
@@ -26,7 +26,7 @@ public interface IFFmpegProcessService
|
||||
DateTimeOffset start,
|
||||
DateTimeOffset finish,
|
||||
DateTimeOffset now,
|
||||
Option<ChannelWatermark> playoutItemWatermark,
|
||||
List<ChannelWatermark> playoutItemWatermarks,
|
||||
Option<ChannelWatermark> globalWatermark,
|
||||
string vaapiDisplay,
|
||||
VaapiDriver vaapiDriver,
|
||||
|
||||
@@ -426,10 +426,13 @@ public class PlayoutBuilder : IPlayoutBuilder
|
||||
// so only log the warning, and leave the bad data to fail tests
|
||||
// playout.Items.Remove(futureItem);
|
||||
|
||||
_logger.LogInformation(
|
||||
"{Count} playout items are scheduled after hard stop of {HardStop}; this is expected if duration is used.",
|
||||
futureItemCount,
|
||||
trimAfter);
|
||||
if (futureItemCount > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"{Count} playout items are scheduled after hard stop of {HardStop}; this is expected if duration is used.",
|
||||
futureItemCount,
|
||||
trimAfter);
|
||||
}
|
||||
}
|
||||
|
||||
return playout;
|
||||
|
||||
@@ -157,13 +157,18 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
|
||||
? FillerKind.GuideMode
|
||||
: FillerKind.None,
|
||||
CustomTitle = scheduleItem.CustomTitle,
|
||||
WatermarkId = scheduleItem.WatermarkId,
|
||||
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||
SubtitleMode = scheduleItem.SubtitleMode
|
||||
};
|
||||
|
||||
if (scheduleItem.WatermarkId is not null)
|
||||
{
|
||||
playoutItem.Watermarks ??= [];
|
||||
playoutItem.Watermarks.Add(scheduleItem.Watermark);
|
||||
}
|
||||
|
||||
durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime);
|
||||
|
||||
DateTimeOffset durationFinish = nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc);
|
||||
|
||||
@@ -76,13 +76,18 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
|
||||
? FillerKind.GuideMode
|
||||
: FillerKind.None,
|
||||
CustomTitle = scheduleItem.CustomTitle,
|
||||
WatermarkId = scheduleItem.WatermarkId,
|
||||
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||
SubtitleMode = scheduleItem.SubtitleMode
|
||||
};
|
||||
|
||||
if (scheduleItem.WatermarkId is not null)
|
||||
{
|
||||
playoutItem.Watermarks ??= [];
|
||||
playoutItem.Watermarks.Add(scheduleItem.Watermark);
|
||||
}
|
||||
|
||||
var enumeratorStates = new Dictionary<CollectionKey, CollectionEnumeratorState>();
|
||||
foreach ((CollectionKey key, IMediaCollectionEnumerator enumerator) in collectionEnumerators)
|
||||
{
|
||||
|
||||
@@ -100,13 +100,18 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
|
||||
? FillerKind.GuideMode
|
||||
: FillerKind.None,
|
||||
CustomTitle = scheduleItem.CustomTitle,
|
||||
WatermarkId = scheduleItem.WatermarkId,
|
||||
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||
SubtitleMode = scheduleItem.SubtitleMode
|
||||
};
|
||||
|
||||
if (scheduleItem.WatermarkId is not null)
|
||||
{
|
||||
playoutItem.Watermarks ??= [];
|
||||
playoutItem.Watermarks.Add(scheduleItem.Watermark);
|
||||
}
|
||||
|
||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||
|
||||
playoutItems.AddRange(
|
||||
|
||||
@@ -47,13 +47,18 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
|
||||
? FillerKind.GuideMode
|
||||
: FillerKind.None,
|
||||
CustomTitle = scheduleItem.CustomTitle,
|
||||
WatermarkId = scheduleItem.WatermarkId,
|
||||
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||
SubtitleMode = scheduleItem.SubtitleMode
|
||||
};
|
||||
|
||||
if (scheduleItem.WatermarkId is not null)
|
||||
{
|
||||
playoutItem.Watermarks ??= [];
|
||||
playoutItem.Watermarks.Add(scheduleItem.Watermark);
|
||||
}
|
||||
|
||||
List<PlayoutItem> playoutItems = AddFiller(
|
||||
playoutBuilderState,
|
||||
collectionEnumerators,
|
||||
|
||||
@@ -58,17 +58,23 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou
|
||||
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||
//SubtitleMode = scheduleItem.SubtitleMode
|
||||
GuideGroup = context.PeekNextGuideGroup()
|
||||
GuideGroup = context.PeekNextGuideGroup(),
|
||||
//GuideStart = effectiveBlock.Start.UtcDateTime,
|
||||
//GuideFinish = blockFinish.UtcDateTime,
|
||||
//BlockKey = JsonConvert.SerializeObject(effectiveBlock.BlockKey),
|
||||
//CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings),
|
||||
//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);
|
||||
|
||||
@@ -83,17 +83,23 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay
|
||||
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||
//SubtitleMode = scheduleItem.SubtitleMode
|
||||
GuideGroup = context.PeekNextGuideGroup()
|
||||
GuideGroup = context.PeekNextGuideGroup(),
|
||||
//GuideStart = effectiveBlock.Start.UtcDateTime,
|
||||
//GuideFinish = blockFinish.UtcDateTime,
|
||||
//BlockKey = JsonConvert.SerializeObject(effectiveBlock.BlockKey),
|
||||
//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);
|
||||
|
||||
@@ -122,12 +122,18 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
|
||||
GuideGroup = context.PeekNextGuideGroup(),
|
||||
FillerKind = fillerKind,
|
||||
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)
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
|
||||
|
||||
public class YamlPlayoutWatermarkHandler(IChannelRepository channelRepository) : IYamlPlayoutHandler
|
||||
{
|
||||
private readonly Dictionary<string, Option<ChannelWatermark>> _watermarkCache = new();
|
||||
|
||||
public bool Reset => false;
|
||||
|
||||
public async Task<bool> Handle(
|
||||
@@ -24,17 +26,48 @@ public class YamlPlayoutWatermarkHandler(IChannelRepository channelRepository) :
|
||||
|
||||
if (watermark.Watermark && !string.IsNullOrWhiteSpace(watermark.Name))
|
||||
{
|
||||
Option<ChannelWatermark> maybeWatermark = await channelRepository.GetWatermarkByName(watermark.Name);
|
||||
foreach (ChannelWatermark channelWatermark in maybeWatermark)
|
||||
foreach (var wm in await GetChannelWatermarkByName(watermark.Name))
|
||||
{
|
||||
context.SetChannelWatermarkId(channelWatermark.Id);
|
||||
context.SetChannelWatermarkId(wm.Id);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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 Stack<FillerKind> _fillerKind = new();
|
||||
private readonly System.Collections.Generic.HashSet<int> _channelWatermarkIds = [];
|
||||
private int _guideGroup = guideGroup;
|
||||
private bool _guideGroupLocked;
|
||||
private int _instructionIndex;
|
||||
private Option<int> _channelWatermarkId;
|
||||
private Option<string> _preRollSequence;
|
||||
private Option<string> _postRollSequence;
|
||||
private Option<MidRollSequence> _midRollSequence;
|
||||
@@ -84,15 +84,20 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -142,19 +147,19 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
|
||||
|
||||
public string Serialize()
|
||||
{
|
||||
int? channelWatermarkId = null;
|
||||
foreach (int id in _channelWatermarkId)
|
||||
{
|
||||
channelWatermarkId = id;
|
||||
}
|
||||
|
||||
string preRollSequence = null;
|
||||
foreach (string sequence in _preRollSequence)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -184,9 +189,9 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
|
||||
_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))
|
||||
@@ -199,7 +204,7 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
|
||||
int? InstructionIndex,
|
||||
int? GuideGroup,
|
||||
bool? GuideGroupLocked,
|
||||
int? ChannelWatermarkId,
|
||||
List<int> ChannelWatermarkIds,
|
||||
string PreRollSequence);
|
||||
|
||||
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")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("WatermarkId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MediaItemId");
|
||||
|
||||
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.ToTable("PlayoutItem", (string)null);
|
||||
b.ToTable("PlayoutItemWatermark");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b =>
|
||||
@@ -4617,14 +4627,26 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
|
||||
.WithMany()
|
||||
.HasForeignKey("WatermarkId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("MediaItem");
|
||||
|
||||
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");
|
||||
});
|
||||
@@ -5748,6 +5770,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
b.Navigation("Playouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelWatermark", b =>
|
||||
{
|
||||
b.Navigation("PlayoutItemWatermarks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b =>
|
||||
{
|
||||
b.Navigation("CollectionItems");
|
||||
@@ -5930,6 +5957,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
b.Navigation("Templates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
|
||||
{
|
||||
b.Navigation("PlayoutItemWatermarks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
|
||||
{
|
||||
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")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("WatermarkId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MediaItemId");
|
||||
|
||||
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.ToTable("PlayoutItem", (string)null);
|
||||
b.ToTable("PlayoutItemWatermark");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b =>
|
||||
@@ -4454,14 +4464,26 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
|
||||
.WithMany()
|
||||
.HasForeignKey("WatermarkId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("MediaItem");
|
||||
|
||||
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");
|
||||
});
|
||||
@@ -5585,6 +5607,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
b.Navigation("Playouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelWatermark", b =>
|
||||
{
|
||||
b.Navigation("PlayoutItemWatermarks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b =>
|
||||
{
|
||||
b.Navigation("CollectionItems");
|
||||
@@ -5767,6 +5794,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
b.Navigation("Templates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
|
||||
{
|
||||
b.Navigation("PlayoutItemWatermarks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
|
||||
@@ -16,10 +16,17 @@ public class PlayoutItemConfiguration : IEntityTypeConfiguration<PlayoutItem>
|
||||
.HasForeignKey(pi => pi.MediaItemId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(i => i.Watermark)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.WatermarkId)
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.IsRequired(false);
|
||||
builder.HasMany(c => c.Watermarks)
|
||||
.WithMany(m => m.PlayoutItems)
|
||||
.UsingEntity<PlayoutItemWatermark>(
|
||||
j => j.HasOne(ci => ci.Watermark)
|
||||
.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
|
||||
{
|
||||
await pipeWriter.CompleteAsync();
|
||||
|
||||
foreach (var element in elements.OfType<IDisposable>())
|
||||
{
|
||||
element.Dispose();
|
||||
|
||||
@@ -43,7 +43,22 @@ public class WatermarkElement : IGraphicsElement, IDisposable
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ public class TranscodingTests
|
||||
now,
|
||||
now + TimeSpan.FromSeconds(3),
|
||||
now,
|
||||
Option<ChannelWatermark>.None,
|
||||
[],
|
||||
GetWatermark(watermark),
|
||||
"drm",
|
||||
VaapiDriver.RadeonSI,
|
||||
@@ -646,7 +646,7 @@ public class TranscodingTests
|
||||
now,
|
||||
now + TimeSpan.FromSeconds(3),
|
||||
now,
|
||||
Option<ChannelWatermark>.None,
|
||||
[],
|
||||
channelWatermark,
|
||||
"drm",
|
||||
VaapiDriver.RadeonSI,
|
||||
|
||||
@@ -97,12 +97,20 @@
|
||||
if (_exists)
|
||||
{
|
||||
_yamlText = await File.ReadAllTextAsync(_yamlFile);
|
||||
_jsonText = YamlScheduleValidator.ToJson(_yamlText);
|
||||
var messages = await YamlScheduleValidator.GetValidationMessages(_yamlText, _isImport);
|
||||
_messagesCount = messages.Count;
|
||||
_messages = string.Join("\n", messages);
|
||||
try
|
||||
{
|
||||
_jsonText = YamlScheduleValidator.ToJson(_yamlText);
|
||||
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" },
|
||||
"content": { "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" ],
|
||||
"additionalProperties": false
|
||||
@@ -191,7 +192,8 @@
|
||||
"count": { "type": "integer" },
|
||||
"content": { "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" ],
|
||||
"additionalProperties": false
|
||||
@@ -207,7 +209,8 @@
|
||||
"offline_tail": { "type": "boolean" },
|
||||
"stop_before_end": { "type": "boolean" },
|
||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||
"custom_title": { "type": "string" }
|
||||
"custom_title": { "type": "string" },
|
||||
"disable_watermarks": { "type": "boolean" }
|
||||
},
|
||||
"required": [ "duration", "content" ],
|
||||
"additionalProperties": false
|
||||
@@ -221,7 +224,8 @@
|
||||
"fallback": { "type": "string" },
|
||||
"discard_attempts": { "type": "integer" },
|
||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||
"custom_title": { "type": "string" }
|
||||
"custom_title": { "type": "string" },
|
||||
"disable_watermarks": { "type": "boolean" }
|
||||
},
|
||||
"required": [ "pad_to_next", "content" ],
|
||||
"additionalProperties": false
|
||||
@@ -238,7 +242,8 @@
|
||||
"discard_attempts": { "type": "integer" },
|
||||
"stop_before_end": { "type": "boolean" },
|
||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||
"custom_title": { "type": "string" }
|
||||
"custom_title": { "type": "string" },
|
||||
"disable_watermarks": { "type": "boolean" }
|
||||
},
|
||||
"required": [ "pad_until", "content" ],
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -229,7 +229,8 @@
|
||||
"all": { "type": "null" },
|
||||
"content": { "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" ],
|
||||
"additionalProperties": false
|
||||
@@ -240,7 +241,8 @@
|
||||
"count": { "type": "integer" },
|
||||
"content": { "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" ],
|
||||
"additionalProperties": false
|
||||
@@ -256,7 +258,8 @@
|
||||
"offline_tail": { "type": "boolean" },
|
||||
"stop_before_end": { "type": "boolean" },
|
||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||
"custom_title": { "type": "string" }
|
||||
"custom_title": { "type": "string" },
|
||||
"disable_watermarks": { "type": "boolean" }
|
||||
},
|
||||
"required": [ "duration", "content" ],
|
||||
"additionalProperties": false
|
||||
@@ -270,7 +273,8 @@
|
||||
"fallback": { "type": "string" },
|
||||
"discard_attempts": { "type": "integer" },
|
||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||
"custom_title": { "type": "string" }
|
||||
"custom_title": { "type": "string" },
|
||||
"disable_watermarks": { "type": "boolean" }
|
||||
},
|
||||
"required": [ "pad_to_next", "content" ],
|
||||
"additionalProperties": false
|
||||
@@ -287,7 +291,8 @@
|
||||
"discard_attempts": { "type": "integer" },
|
||||
"stop_before_end": { "type": "boolean" },
|
||||
"filler_kind": { "$ref": "#/$defs/enums/filler_kind" },
|
||||
"custom_title": { "type": "string" }
|
||||
"custom_title": { "type": "string" },
|
||||
"disable_watermarks": { "type": "boolean" }
|
||||
},
|
||||
"required": [ "pad_until", "content" ],
|
||||
"additionalProperties": false
|
||||
|
||||
Reference in New Issue
Block a user