diff --git a/CHANGELOG.md b/CHANGELOG.md index d88c33ff7..ff8a42b2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Like `Other Videos`, `Songs` require no metadata or particular folder layout, and will have tags added for each containing folder - For Example, a song at `rock/band/1990 - Album/01 whatever.flac` will have the tags `rock`, `band` and `1990 - Album`, and the title `01 whatever` - Channels that play songs *require* fallback filler to provide looping video to pair with the songs +- Add `Covert Art` watermark source to support displaying embedded cover art from songs +- Add support for `.webm` video files ## [0.2.5-alpha] - 2021-11-21 ### Fixed diff --git a/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs b/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs index 5397a5905..2fa037fff 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs @@ -274,7 +274,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg HorizontalMarginPercent = 7, VerticalMarginPercent = 5 }), - new Resolution { Width = 1920, Height = 1080 }) + new Resolution { Width = 1920, Height = 1080 }, + None) .WithDeinterlace(deinterlace) .WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None); diff --git a/ErsatzTV.Core/Domain/ChannelWatermark.cs b/ErsatzTV.Core/Domain/ChannelWatermark.cs index c4e81f7b0..c0c5bfc3c 100644 --- a/ErsatzTV.Core/Domain/ChannelWatermark.cs +++ b/ErsatzTV.Core/Domain/ChannelWatermark.cs @@ -45,6 +45,7 @@ public enum ChannelWatermarkImageSource { Custom = 0, - ChannelLogo = 1 + ChannelLogo = 1, + CoverArt = 2 } } diff --git a/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs b/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs index 8e37679b5..6ebd233b1 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs @@ -21,6 +21,7 @@ namespace ErsatzTV.Core.FFmpeg private IDisplaySize _resolution; private Option _scaleToSize = None; private Option _watermark; + private Option _watermarkIndex; private string _pixelFormat; private string _videoEncoder; @@ -80,10 +81,14 @@ namespace ErsatzTV.Core.FFmpeg return this; } - public FFmpegComplexFilterBuilder WithWatermark(Option watermark, IDisplaySize resolution) + public FFmpegComplexFilterBuilder WithWatermark( + Option watermark, + IDisplaySize resolution, + Option watermarkIndex) { _watermark = watermark; _resolution = resolution; + _watermarkIndex = watermarkIndex; return this; } @@ -315,6 +320,11 @@ namespace ErsatzTV.Core.FFmpeg } var watermarkLabel = $"[{audioInput+1}:v]"; + foreach (int index in _watermarkIndex) + { + watermarkLabel = $"[{audioInput+1}:{index}]"; + } + if (!string.IsNullOrWhiteSpace(watermarkPreprocess)) { complexFilter.Append($"{watermarkLabel}{watermarkPreprocess}[wmp];"); diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs index 38f2ef718..551786205 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs @@ -187,8 +187,8 @@ namespace ErsatzTV.Core.FFmpeg return this; } - public FFmpegProcessBuilder WithWatermarks( - List watermarkOptions, + public FFmpegProcessBuilder WithWatermark( + Option watermarkOptions, IDisplaySize resolution) { foreach (WatermarkOptions options in watermarkOptions) @@ -204,10 +204,11 @@ namespace ErsatzTV.Core.FFmpeg _arguments.Add("-i"); _arguments.Add(path); - _complexFilterBuilder = _complexFilterBuilder.WithWatermark(options.Watermark, resolution); + _complexFilterBuilder = _complexFilterBuilder.WithWatermark( + options.Watermark, + resolution, + options.ImageStreamIndex); } - - // TODO: when image path is null? } return this; diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs index e70019a38..43fe30c26 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading.Tasks; @@ -65,8 +64,8 @@ namespace ErsatzTV.Core.FFmpeg inPoint, outPoint); - List watermarkOptions = - await GetAllWatermarkOptions(channel, globalWatermark, videoStream); + Option watermarkOptions = + await GetWatermarkOptions(channel, globalWatermark, audioVersion); FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger) .WithThreads(playbackSettings.ThreadCount) @@ -86,7 +85,7 @@ namespace ErsatzTV.Core.FFmpeg playbackSettings.VideoDecoder, videoStream.Codec, videoStream.PixelFormat) - .WithWatermarks(watermarkOptions, channel.FFmpegProfile.Resolution) + .WithWatermark(watermarkOptions, channel.FFmpegProfile.Resolution) .WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale) .WithAlignedAudio(playbackSettings.AudioDuration) .WithNormalizeLoudness(playbackSettings.NormalizeLoudness); @@ -226,35 +225,10 @@ namespace ErsatzTV.Core.FFmpeg private bool NeedToPad(IDisplaySize target, IDisplaySize displaySize) => displaySize.Width != target.Width || displaySize.Height != target.Height; - private async Task> GetAllWatermarkOptions( + private async Task GetWatermarkOptions( Channel channel, Option globalWatermark, - Option maybeVideoStream) - { - var result = new List(); - foreach (WatermarkOptions options in Optional(await GetWatermarkOptions(channel, globalWatermark))) - { - result.Add(options); - } - - foreach (MediaStream videoStream in maybeVideoStream.Where(s => s.AttachedPic)) - { - // TODO: use attached pic as watermark - - // var options = new WatermarkOptions( - // new ChannelWatermark - // { - // }, - // None, - // false); - // - // result.Add(options); - } - - return result; - } - - private async Task GetWatermarkOptions(Channel channel, Option globalWatermark) + MediaVersion audioVersion) { if (channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect && channel.FFmpegProfile.Transcode && channel.FFmpegProfile.NormalizeVideo) @@ -269,7 +243,11 @@ namespace ErsatzTV.Core.FFmpeg channel.Watermark.Image, ArtworkKind.Watermark, Option.None); - return new WatermarkOptions(channel.Watermark, customPath, await _imageCache.IsAnimated(customPath)); + return new WatermarkOptions( + channel.Watermark, + customPath, + None, + await _imageCache.IsAnimated(customPath)); case ChannelWatermarkImageSource.ChannelLogo: Option maybeChannelPath = channel.Artwork .Filter(a => a.ArtworkKind == ArtworkKind.Logo) @@ -278,9 +256,26 @@ namespace ErsatzTV.Core.FFmpeg return new WatermarkOptions( channel.Watermark, maybeChannelPath, + None, await maybeChannelPath.Match( p => _imageCache.IsAnimated(p), () => Task.FromResult(false))); + case ChannelWatermarkImageSource.CoverArt: + Option maybeAttachedPicStream = + Optional(audioVersion.Streams.Find(s => s.AttachedPic)); + + // only return a watermark if there is an attachment + // to allow falling back on a global watermark + foreach (MediaStream attachedPicStream in maybeAttachedPicStream) + { + return new WatermarkOptions( + channel.Watermark, + audioVersion.MediaFiles.Head().Path, + attachedPicStream.Index, + false); + } + + break; default: throw new NotSupportedException("Unsupported watermark image source"); } @@ -296,7 +291,11 @@ namespace ErsatzTV.Core.FFmpeg watermark.Image, ArtworkKind.Watermark, Option.None); - return new WatermarkOptions(watermark, customPath, await _imageCache.IsAnimated(customPath)); + return new WatermarkOptions( + watermark, + customPath, + None, + await _imageCache.IsAnimated(customPath)); case ChannelWatermarkImageSource.ChannelLogo: Option maybeChannelPath = channel.Artwork .Filter(a => a.ArtworkKind == ArtworkKind.Logo) @@ -305,16 +304,32 @@ namespace ErsatzTV.Core.FFmpeg return new WatermarkOptions( watermark, maybeChannelPath, + None, await maybeChannelPath.Match( p => _imageCache.IsAnimated(p), () => Task.FromResult(false))); + case ChannelWatermarkImageSource.CoverArt: + Option maybeAttachedPicStream = + Optional(audioVersion.Streams.Find(s => s.AttachedPic)); + + // only return a watermark if there is an attachment + foreach (MediaStream attachedPicStream in maybeAttachedPicStream) + { + return new WatermarkOptions( + channel.Watermark, + audioVersion.MediaFiles.Head().Path, + attachedPicStream.Index, + false); + } + + break; default: throw new NotSupportedException("Unsupported watermark image source"); } } } - return new WatermarkOptions(None, None, false); + return new WatermarkOptions(None, None, None, false); } } } diff --git a/ErsatzTV.Core/FFmpeg/WatermarkOptions.cs b/ErsatzTV.Core/FFmpeg/WatermarkOptions.cs index 875bd29ae..43ddfe2b8 100644 --- a/ErsatzTV.Core/FFmpeg/WatermarkOptions.cs +++ b/ErsatzTV.Core/FFmpeg/WatermarkOptions.cs @@ -3,5 +3,9 @@ using LanguageExt; namespace ErsatzTV.Core.FFmpeg { - public record WatermarkOptions(Option Watermark, Option ImagePath, bool IsAnimated); + public record WatermarkOptions( + Option Watermark, + Option ImagePath, + Option ImageStreamIndex, + bool IsAnimated); } diff --git a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs index 5e14ab339..b1e7b02a6 100644 --- a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs @@ -18,7 +18,7 @@ namespace ErsatzTV.Core.Metadata public static readonly List VideoFileExtensions = new() { ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4", - ".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".ts" + ".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".ts", ".webm" }; public static readonly List AudioFileExtensions = new() diff --git a/ErsatzTV/Pages/WatermarkEditor.razor b/ErsatzTV/Pages/WatermarkEditor.razor index e91323bf5..ef04f4dba 100644 --- a/ErsatzTV/Pages/WatermarkEditor.razor +++ b/ErsatzTV/Pages/WatermarkEditor.razor @@ -31,11 +31,12 @@ Disabled="@(_model.Mode == ChannelWatermarkMode.None)"> Custom Channel Logo + Cover Art