Browse Source

add cover art watermark source (#491)

* add cover art watermark source

* update changelog
pull/492/head
Jason Dove 5 years ago committed by GitHub
parent
commit
ac048b72ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 3
      ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs
  3. 3
      ErsatzTV.Core/Domain/ChannelWatermark.cs
  4. 12
      ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs
  5. 11
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  6. 83
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  7. 6
      ErsatzTV.Core/FFmpeg/WatermarkOptions.cs
  8. 2
      ErsatzTV.Core/Metadata/LocalFolderScanner.cs
  9. 5
      ErsatzTV/Pages/WatermarkEditor.razor
  10. 4
      ErsatzTV/Pages/Watermarks.razor
  11. 5
      ErsatzTV/Validators/WatermarkEditViewModelValidator.cs

2
CHANGELOG.md

@ -13,6 +13,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

3
ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs

@ -274,7 +274,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -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);

3
ErsatzTV.Core/Domain/ChannelWatermark.cs

@ -45,6 +45,7 @@ @@ -45,6 +45,7 @@
public enum ChannelWatermarkImageSource
{
Custom = 0,
ChannelLogo = 1
ChannelLogo = 1,
CoverArt = 2
}
}

12
ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs

@ -21,6 +21,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -21,6 +21,7 @@ namespace ErsatzTV.Core.FFmpeg
private IDisplaySize _resolution;
private Option<IDisplaySize> _scaleToSize = None;
private Option<ChannelWatermark> _watermark;
private Option<int> _watermarkIndex;
private string _pixelFormat;
private string _videoEncoder;
@ -80,10 +81,14 @@ namespace ErsatzTV.Core.FFmpeg @@ -80,10 +81,14 @@ namespace ErsatzTV.Core.FFmpeg
return this;
}
public FFmpegComplexFilterBuilder WithWatermark(Option<ChannelWatermark> watermark, IDisplaySize resolution)
public FFmpegComplexFilterBuilder WithWatermark(
Option<ChannelWatermark> watermark,
IDisplaySize resolution,
Option<int> watermarkIndex)
{
_watermark = watermark;
_resolution = resolution;
_watermarkIndex = watermarkIndex;
return this;
}
@ -315,6 +320,11 @@ namespace ErsatzTV.Core.FFmpeg @@ -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];");

11
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -187,8 +187,8 @@ namespace ErsatzTV.Core.FFmpeg @@ -187,8 +187,8 @@ namespace ErsatzTV.Core.FFmpeg
return this;
}
public FFmpegProcessBuilder WithWatermarks(
List<WatermarkOptions> watermarkOptions,
public FFmpegProcessBuilder WithWatermark(
Option<WatermarkOptions> watermarkOptions,
IDisplaySize resolution)
{
foreach (WatermarkOptions options in watermarkOptions)
@ -204,10 +204,11 @@ namespace ErsatzTV.Core.FFmpeg @@ -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;

83
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -1,5 +1,4 @@ @@ -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 @@ -65,8 +64,8 @@ namespace ErsatzTV.Core.FFmpeg
inPoint,
outPoint);
List<WatermarkOptions> watermarkOptions =
await GetAllWatermarkOptions(channel, globalWatermark, videoStream);
Option<WatermarkOptions> watermarkOptions =
await GetWatermarkOptions(channel, globalWatermark, audioVersion);
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
.WithThreads(playbackSettings.ThreadCount)
@ -86,7 +85,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -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 @@ -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<List<WatermarkOptions>> GetAllWatermarkOptions(
private async Task<WatermarkOptions> GetWatermarkOptions(
Channel channel,
Option<ChannelWatermark> globalWatermark,
Option<MediaStream> maybeVideoStream)
{
var result = new List<WatermarkOptions>();
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<WatermarkOptions> GetWatermarkOptions(Channel channel, Option<ChannelWatermark> globalWatermark)
MediaVersion audioVersion)
{
if (channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect && channel.FFmpegProfile.Transcode &&
channel.FFmpegProfile.NormalizeVideo)
@ -269,7 +243,11 @@ namespace ErsatzTV.Core.FFmpeg @@ -269,7 +243,11 @@ namespace ErsatzTV.Core.FFmpeg
channel.Watermark.Image,
ArtworkKind.Watermark,
Option<int>.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<string> maybeChannelPath = channel.Artwork
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
@ -278,9 +256,26 @@ namespace ErsatzTV.Core.FFmpeg @@ -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<MediaStream> 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 @@ -296,7 +291,11 @@ namespace ErsatzTV.Core.FFmpeg
watermark.Image,
ArtworkKind.Watermark,
Option<int>.None);
return new WatermarkOptions(watermark, customPath, await _imageCache.IsAnimated(customPath));
return new WatermarkOptions(
watermark,
customPath,
None,
await _imageCache.IsAnimated(customPath));
case ChannelWatermarkImageSource.ChannelLogo:
Option<string> maybeChannelPath = channel.Artwork
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
@ -305,16 +304,32 @@ namespace ErsatzTV.Core.FFmpeg @@ -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<MediaStream> 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);
}
}
}

6
ErsatzTV.Core/FFmpeg/WatermarkOptions.cs

@ -3,5 +3,9 @@ using LanguageExt; @@ -3,5 +3,9 @@ using LanguageExt;
namespace ErsatzTV.Core.FFmpeg
{
public record WatermarkOptions(Option<ChannelWatermark> Watermark, Option<string> ImagePath, bool IsAnimated);
public record WatermarkOptions(
Option<ChannelWatermark> Watermark,
Option<string> ImagePath,
Option<int> ImageStreamIndex,
bool IsAnimated);
}

2
ErsatzTV.Core/Metadata/LocalFolderScanner.cs

@ -18,7 +18,7 @@ namespace ErsatzTV.Core.Metadata @@ -18,7 +18,7 @@ namespace ErsatzTV.Core.Metadata
public static readonly List<string> 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<string> AudioFileExtensions = new()

5
ErsatzTV/Pages/WatermarkEditor.razor

@ -31,11 +31,12 @@ @@ -31,11 +31,12 @@
Disabled="@(_model.Mode == ChannelWatermarkMode.None)">
<MudSelectItem Value="@(ChannelWatermarkImageSource.Custom)">Custom</MudSelectItem>
<MudSelectItem Value="@(ChannelWatermarkImageSource.ChannelLogo)">Channel Logo</MudSelectItem>
<MudSelectItem Value="@(ChannelWatermarkImageSource.CoverArt)">Cover Art</MudSelectItem>
</MudSelect>
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
<MudItem xs="6">
<InputFile id="watermarkFileInput" OnChange="UploadWatermark" style="display: none;"/>
@if (!string.IsNullOrWhiteSpace(_model.Image) && _model.ImageSource != ChannelWatermarkImageSource.ChannelLogo)
@if (!string.IsNullOrWhiteSpace(_model.Image) && _model.ImageSource == ChannelWatermarkImageSource.Custom)
{
<MudElement HtmlTag="img" src="@($"artwork/watermarks/{_model.Image}")" Style="max-height: 50px"/>
}
@ -46,7 +47,7 @@ @@ -46,7 +47,7 @@
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.CloudUpload"
Disabled="@(_model.ImageSource == ChannelWatermarkImageSource.ChannelLogo)"
Disabled="@(_model.ImageSource != ChannelWatermarkImageSource.Custom)"
for="watermarkFileInput">
Upload Image
</MudButton>

4
ErsatzTV/Pages/Watermarks.razor

@ -38,6 +38,10 @@ @@ -38,6 +38,10 @@
{
<MudText>[channel logo]</MudText>
}
else if (context.ImageSource == ChannelWatermarkImageSource.CoverArt)
{
<MudText>[cover art]</MudText>
}
</MudTd>
<MudTd DataLabel="Mode">
@context.Mode

5
ErsatzTV/Validators/WatermarkEditViewModelValidator.cs

@ -9,10 +9,11 @@ namespace ErsatzTV.Validators @@ -9,10 +9,11 @@ namespace ErsatzTV.Validators
public WatermarkEditViewModelValidator()
{
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.Image)
.NotEmpty()
.WithMessage("Watermark image is required!");
.WithMessage("Watermark image is required!")
.When(vm => vm.ImageSource == ChannelWatermarkImageSource.Custom);
RuleFor(x => x.Width)
.GreaterThan(0)

Loading…
Cancel
Save