add cover art watermark source (#491)
* add cover art watermark source * update changelog
This commit is contained in:
@@ -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
|
- 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`
|
- 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
|
- 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
|
## [0.2.5-alpha] - 2021-11-21
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -274,7 +274,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
HorizontalMarginPercent = 7,
|
HorizontalMarginPercent = 7,
|
||||||
VerticalMarginPercent = 5
|
VerticalMarginPercent = 5
|
||||||
}),
|
}),
|
||||||
new Resolution { Width = 1920, Height = 1080 })
|
new Resolution { Width = 1920, Height = 1080 },
|
||||||
|
None)
|
||||||
.WithDeinterlace(deinterlace)
|
.WithDeinterlace(deinterlace)
|
||||||
.WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None);
|
.WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None);
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
public enum ChannelWatermarkImageSource
|
public enum ChannelWatermarkImageSource
|
||||||
{
|
{
|
||||||
Custom = 0,
|
Custom = 0,
|
||||||
ChannelLogo = 1
|
ChannelLogo = 1,
|
||||||
|
CoverArt = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
private IDisplaySize _resolution;
|
private IDisplaySize _resolution;
|
||||||
private Option<IDisplaySize> _scaleToSize = None;
|
private Option<IDisplaySize> _scaleToSize = None;
|
||||||
private Option<ChannelWatermark> _watermark;
|
private Option<ChannelWatermark> _watermark;
|
||||||
|
private Option<int> _watermarkIndex;
|
||||||
private string _pixelFormat;
|
private string _pixelFormat;
|
||||||
private string _videoEncoder;
|
private string _videoEncoder;
|
||||||
|
|
||||||
@@ -80,10 +81,14 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FFmpegComplexFilterBuilder WithWatermark(Option<ChannelWatermark> watermark, IDisplaySize resolution)
|
public FFmpegComplexFilterBuilder WithWatermark(
|
||||||
|
Option<ChannelWatermark> watermark,
|
||||||
|
IDisplaySize resolution,
|
||||||
|
Option<int> watermarkIndex)
|
||||||
{
|
{
|
||||||
_watermark = watermark;
|
_watermark = watermark;
|
||||||
_resolution = resolution;
|
_resolution = resolution;
|
||||||
|
_watermarkIndex = watermarkIndex;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,6 +320,11 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
}
|
}
|
||||||
|
|
||||||
var watermarkLabel = $"[{audioInput+1}:v]";
|
var watermarkLabel = $"[{audioInput+1}:v]";
|
||||||
|
foreach (int index in _watermarkIndex)
|
||||||
|
{
|
||||||
|
watermarkLabel = $"[{audioInput+1}:{index}]";
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(watermarkPreprocess))
|
if (!string.IsNullOrWhiteSpace(watermarkPreprocess))
|
||||||
{
|
{
|
||||||
complexFilter.Append($"{watermarkLabel}{watermarkPreprocess}[wmp];");
|
complexFilter.Append($"{watermarkLabel}{watermarkPreprocess}[wmp];");
|
||||||
|
|||||||
@@ -187,8 +187,8 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FFmpegProcessBuilder WithWatermarks(
|
public FFmpegProcessBuilder WithWatermark(
|
||||||
List<WatermarkOptions> watermarkOptions,
|
Option<WatermarkOptions> watermarkOptions,
|
||||||
IDisplaySize resolution)
|
IDisplaySize resolution)
|
||||||
{
|
{
|
||||||
foreach (WatermarkOptions options in watermarkOptions)
|
foreach (WatermarkOptions options in watermarkOptions)
|
||||||
@@ -204,10 +204,11 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
_arguments.Add("-i");
|
_arguments.Add("-i");
|
||||||
_arguments.Add(path);
|
_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;
|
return this;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -65,8 +64,8 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
inPoint,
|
inPoint,
|
||||||
outPoint);
|
outPoint);
|
||||||
|
|
||||||
List<WatermarkOptions> watermarkOptions =
|
Option<WatermarkOptions> watermarkOptions =
|
||||||
await GetAllWatermarkOptions(channel, globalWatermark, videoStream);
|
await GetWatermarkOptions(channel, globalWatermark, audioVersion);
|
||||||
|
|
||||||
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
|
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
|
||||||
.WithThreads(playbackSettings.ThreadCount)
|
.WithThreads(playbackSettings.ThreadCount)
|
||||||
@@ -86,7 +85,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
playbackSettings.VideoDecoder,
|
playbackSettings.VideoDecoder,
|
||||||
videoStream.Codec,
|
videoStream.Codec,
|
||||||
videoStream.PixelFormat)
|
videoStream.PixelFormat)
|
||||||
.WithWatermarks(watermarkOptions, channel.FFmpegProfile.Resolution)
|
.WithWatermark(watermarkOptions, channel.FFmpegProfile.Resolution)
|
||||||
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale)
|
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale)
|
||||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||||
.WithNormalizeLoudness(playbackSettings.NormalizeLoudness);
|
.WithNormalizeLoudness(playbackSettings.NormalizeLoudness);
|
||||||
@@ -226,35 +225,10 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
private bool NeedToPad(IDisplaySize target, IDisplaySize displaySize) =>
|
private bool NeedToPad(IDisplaySize target, IDisplaySize displaySize) =>
|
||||||
displaySize.Width != target.Width || displaySize.Height != target.Height;
|
displaySize.Width != target.Width || displaySize.Height != target.Height;
|
||||||
|
|
||||||
private async Task<List<WatermarkOptions>> GetAllWatermarkOptions(
|
private async Task<WatermarkOptions> GetWatermarkOptions(
|
||||||
Channel channel,
|
Channel channel,
|
||||||
Option<ChannelWatermark> globalWatermark,
|
Option<ChannelWatermark> globalWatermark,
|
||||||
Option<MediaStream> maybeVideoStream)
|
MediaVersion audioVersion)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
if (channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect && channel.FFmpegProfile.Transcode &&
|
if (channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect && channel.FFmpegProfile.Transcode &&
|
||||||
channel.FFmpegProfile.NormalizeVideo)
|
channel.FFmpegProfile.NormalizeVideo)
|
||||||
@@ -269,7 +243,11 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
channel.Watermark.Image,
|
channel.Watermark.Image,
|
||||||
ArtworkKind.Watermark,
|
ArtworkKind.Watermark,
|
||||||
Option<int>.None);
|
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:
|
case ChannelWatermarkImageSource.ChannelLogo:
|
||||||
Option<string> maybeChannelPath = channel.Artwork
|
Option<string> maybeChannelPath = channel.Artwork
|
||||||
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
|
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
|
||||||
@@ -278,9 +256,26 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
return new WatermarkOptions(
|
return new WatermarkOptions(
|
||||||
channel.Watermark,
|
channel.Watermark,
|
||||||
maybeChannelPath,
|
maybeChannelPath,
|
||||||
|
None,
|
||||||
await maybeChannelPath.Match(
|
await maybeChannelPath.Match(
|
||||||
p => _imageCache.IsAnimated(p),
|
p => _imageCache.IsAnimated(p),
|
||||||
() => Task.FromResult(false)));
|
() => 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:
|
default:
|
||||||
throw new NotSupportedException("Unsupported watermark image source");
|
throw new NotSupportedException("Unsupported watermark image source");
|
||||||
}
|
}
|
||||||
@@ -296,7 +291,11 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
watermark.Image,
|
watermark.Image,
|
||||||
ArtworkKind.Watermark,
|
ArtworkKind.Watermark,
|
||||||
Option<int>.None);
|
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:
|
case ChannelWatermarkImageSource.ChannelLogo:
|
||||||
Option<string> maybeChannelPath = channel.Artwork
|
Option<string> maybeChannelPath = channel.Artwork
|
||||||
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
|
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
|
||||||
@@ -305,16 +304,32 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
return new WatermarkOptions(
|
return new WatermarkOptions(
|
||||||
watermark,
|
watermark,
|
||||||
maybeChannelPath,
|
maybeChannelPath,
|
||||||
|
None,
|
||||||
await maybeChannelPath.Match(
|
await maybeChannelPath.Match(
|
||||||
p => _imageCache.IsAnimated(p),
|
p => _imageCache.IsAnimated(p),
|
||||||
() => Task.FromResult(false)));
|
() => 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:
|
default:
|
||||||
throw new NotSupportedException("Unsupported watermark image source");
|
throw new NotSupportedException("Unsupported watermark image source");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WatermarkOptions(None, None, false);
|
return new WatermarkOptions(None, None, None, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,9 @@ using LanguageExt;
|
|||||||
|
|
||||||
namespace ErsatzTV.Core.FFmpeg
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
public static readonly List<string> VideoFileExtensions = new()
|
public static readonly List<string> VideoFileExtensions = new()
|
||||||
{
|
{
|
||||||
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4",
|
".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()
|
public static readonly List<string> AudioFileExtensions = new()
|
||||||
|
|||||||
@@ -31,11 +31,12 @@
|
|||||||
Disabled="@(_model.Mode == ChannelWatermarkMode.None)">
|
Disabled="@(_model.Mode == ChannelWatermarkMode.None)">
|
||||||
<MudSelectItem Value="@(ChannelWatermarkImageSource.Custom)">Custom</MudSelectItem>
|
<MudSelectItem Value="@(ChannelWatermarkImageSource.Custom)">Custom</MudSelectItem>
|
||||||
<MudSelectItem Value="@(ChannelWatermarkImageSource.ChannelLogo)">Channel Logo</MudSelectItem>
|
<MudSelectItem Value="@(ChannelWatermarkImageSource.ChannelLogo)">Channel Logo</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@(ChannelWatermarkImageSource.CoverArt)">Cover Art</MudSelectItem>
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
|
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
|
||||||
<MudItem xs="6">
|
<MudItem xs="6">
|
||||||
<InputFile id="watermarkFileInput" OnChange="UploadWatermark" style="display: none;"/>
|
<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"/>
|
<MudElement HtmlTag="img" src="@($"artwork/watermarks/{_model.Image}")" Style="max-height: 50px"/>
|
||||||
}
|
}
|
||||||
@@ -46,7 +47,7 @@
|
|||||||
Variant="Variant.Filled"
|
Variant="Variant.Filled"
|
||||||
Color="Color.Primary"
|
Color="Color.Primary"
|
||||||
StartIcon="@Icons.Material.Filled.CloudUpload"
|
StartIcon="@Icons.Material.Filled.CloudUpload"
|
||||||
Disabled="@(_model.ImageSource == ChannelWatermarkImageSource.ChannelLogo)"
|
Disabled="@(_model.ImageSource != ChannelWatermarkImageSource.Custom)"
|
||||||
for="watermarkFileInput">
|
for="watermarkFileInput">
|
||||||
Upload Image
|
Upload Image
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
|||||||
@@ -38,6 +38,10 @@
|
|||||||
{
|
{
|
||||||
<MudText>[channel logo]</MudText>
|
<MudText>[channel logo]</MudText>
|
||||||
}
|
}
|
||||||
|
else if (context.ImageSource == ChannelWatermarkImageSource.CoverArt)
|
||||||
|
{
|
||||||
|
<MudText>[cover art]</MudText>
|
||||||
|
}
|
||||||
</MudTd>
|
</MudTd>
|
||||||
<MudTd DataLabel="Mode">
|
<MudTd DataLabel="Mode">
|
||||||
@context.Mode
|
@context.Mode
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ namespace ErsatzTV.Validators
|
|||||||
|
|
||||||
RuleFor(x => x.Image)
|
RuleFor(x => x.Image)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithMessage("Watermark image is required!");
|
.WithMessage("Watermark image is required!")
|
||||||
|
.When(vm => vm.ImageSource == ChannelWatermarkImageSource.Custom);
|
||||||
|
|
||||||
RuleFor(x => x.Width)
|
RuleFor(x => x.Width)
|
||||||
.GreaterThan(0)
|
.GreaterThan(0)
|
||||||
|
|||||||
Reference in New Issue
Block a user