allow animated channel watermarks (#255)

This commit is contained in:
Jason Dove
2021-06-12 06:16:52 -05:00
committed by GitHub
parent 0365d4c8f8
commit 0750a0712f
5 changed files with 80 additions and 22 deletions
+8 -4
View File
@@ -155,15 +155,19 @@ namespace ErsatzTV.Core.FFmpeg
public FFmpegProcessBuilder WithWatermark(
Option<ChannelWatermark> watermark,
Option<string> maybePath,
IDisplaySize resolution)
IDisplaySize resolution,
bool isAnimated)
{
foreach (string path in maybePath)
{
string subfolder = path[..2];
string fullPath = Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder, path);
if (isAnimated)
{
_arguments.Add("-ignore_loop");
_arguments.Add("0");
}
_arguments.Add("-i");
_arguments.Add(fullPath);
_arguments.Add(path);
_complexFilterBuilder = _complexFilterBuilder.WithWatermark(watermark, resolution);
}
+11 -3
View File
@@ -4,6 +4,7 @@ using System.IO;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using LanguageExt;
namespace ErsatzTV.Core.FFmpeg
@@ -11,14 +12,17 @@ namespace ErsatzTV.Core.FFmpeg
public class FFmpegProcessService
{
private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
private readonly IImageCache _imageCache;
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator;
public FFmpegProcessService(
FFmpegPlaybackSettingsCalculator ffmpegPlaybackSettingsService,
IFFmpegStreamSelector ffmpegStreamSelector)
IFFmpegStreamSelector ffmpegStreamSelector,
IImageCache imageCache)
{
_playbackSettingsCalculator = ffmpegPlaybackSettingsService;
_ffmpegStreamSelector = ffmpegStreamSelector;
_imageCache = imageCache;
}
public async Task<Process> ForPlayoutItem(
@@ -46,7 +50,11 @@ namespace ErsatzTV.Core.FFmpeg
.Filter(_ => channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect)
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
.HeadOrNone()
.Map(a => a.Path);
.Map(a => _imageCache.GetPathForImage(a.Path, ArtworkKind.Logo, Option<int>.None));
bool isAnimated = await maybeWatermarkPath.Match(
p => _imageCache.IsAnimated(p),
() => Task.FromResult(false));
Option<ChannelWatermark> maybeWatermark = channel.Watermark;
@@ -58,7 +66,7 @@ namespace ErsatzTV.Core.FFmpeg
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
.WithSeek(playbackSettings.StreamSeek)
.WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec)
.WithWatermark(maybeWatermark, maybeWatermarkPath, channel.FFmpegProfile.Resolution)
.WithWatermark(maybeWatermark, maybeWatermarkPath, channel.FFmpegProfile.Resolution, isAnimated)
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale)
.WithAlignedAudio(playbackSettings.AudioDuration)
.WithNormalizeLoudness(playbackSettings.NormalizeLoudness);
@@ -10,5 +10,6 @@ namespace ErsatzTV.Core.Interfaces.Images
Task<Either<BaseError, string>> SaveArtworkToCache(byte[] imageBuffer, ArtworkKind artworkKind);
Task<Either<BaseError, string>> CopyArtworkToCache(string path, ArtworkKind artworkKind);
string GetPathForImage(string fileName, ArtworkKind artworkKind, Option<int> maybeMaxHeight);
Task<bool> IsAnimated(string fileName);
}
}
+33 -1
View File
@@ -8,6 +8,8 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using LanguageExt;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
@@ -18,10 +20,17 @@ namespace ErsatzTV.Infrastructure.Images
{
private static readonly SHA1CryptoServiceProvider Crypto;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<ImageCache> _logger;
private readonly IMemoryCache _memoryCache;
static ImageCache() => Crypto = new SHA1CryptoServiceProvider();
public ImageCache(ILocalFileSystem localFileSystem) => _localFileSystem = localFileSystem;
public ImageCache(ILocalFileSystem localFileSystem, IMemoryCache memoryCache, ILogger<ImageCache> logger)
{
_localFileSystem = localFileSystem;
_memoryCache = memoryCache;
_logger = logger;
}
public async Task<Either<BaseError, byte[]>> ResizeImage(byte[] imageBuffer, int height)
{
@@ -120,5 +129,28 @@ namespace ErsatzTV.Infrastructure.Images
return Path.Combine(baseFolder, fileName);
}
public async Task<bool> IsAnimated(string fileName)
{
try
{
var cacheKey = $"image.animated.{Path.GetFileName(fileName)}";
if (_memoryCache.TryGetValue(cacheKey, out bool animated))
{
return animated;
}
using Image image = await Image.LoadAsync(fileName);
animated = image.Frames.Count > 1;
_memoryCache.Set(cacheKey, animated, TimeSpan.FromDays(1));
return animated;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unable to check image for animation");
return false;
}
}
}
}
+27 -14
View File
@@ -248,20 +248,33 @@
private async Task UploadLogo(InputFileChangeEventArgs e)
{
var buffer = new byte[e.File.Size];
await e.File.OpenReadStream().ReadAsync(buffer);
Either<BaseError, string> maybeCacheFileName = await _mediator.Send(new SaveArtworkToDisk(buffer, ArtworkKind.Logo));
maybeCacheFileName.Match(
relativeFileName =>
{
_model.Logo = relativeFileName;
StateHasChanged();
},
error =>
{
_snackbar.Add($"Unexpected error saving channel logo: {error.Value}", Severity.Error);
_logger.LogError("Unexpected error saving channel logo: {Error}", error.Value);
});
try
{
var buffer = new byte[e.File.Size];
await e.File.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024).ReadAsync(buffer);
Either<BaseError, string> maybeCacheFileName = await _mediator.Send(new SaveArtworkToDisk(buffer, ArtworkKind.Logo));
maybeCacheFileName.Match(
relativeFileName =>
{
_model.Logo = relativeFileName;
StateHasChanged();
},
error =>
{
_snackbar.Add($"Unexpected error saving channel logo: {error.Value}", Severity.Error);
_logger.LogError("Unexpected error saving channel logo: {Error}", error.Value);
});
}
catch (IOException)
{
_snackbar.Add("Channel logo exceeds maximum allowed file size of 10 MB", Severity.Error);
_logger.LogError("Channel logo exceeds maximum allowed file size of 10 MB");
}
catch (Exception ex)
{
_snackbar.Add($"Unexpected error saving channel logo: {ex.Message}", Severity.Error);
_logger.LogError("Unexpected error saving channel logo: {Error}", ex.Message);
}
}
}