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( public FFmpegProcessBuilder WithWatermark(
Option<ChannelWatermark> watermark, Option<ChannelWatermark> watermark,
Option<string> maybePath, Option<string> maybePath,
IDisplaySize resolution) IDisplaySize resolution,
bool isAnimated)
{ {
foreach (string path in maybePath) foreach (string path in maybePath)
{ {
string subfolder = path[..2]; if (isAnimated)
string fullPath = Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder, path); {
_arguments.Add("-ignore_loop");
_arguments.Add("0");
}
_arguments.Add("-i"); _arguments.Add("-i");
_arguments.Add(fullPath); _arguments.Add(path);
_complexFilterBuilder = _complexFilterBuilder.WithWatermark(watermark, resolution); _complexFilterBuilder = _complexFilterBuilder.WithWatermark(watermark, resolution);
} }
+11 -3
View File
@@ -4,6 +4,7 @@ using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using LanguageExt; using LanguageExt;
namespace ErsatzTV.Core.FFmpeg namespace ErsatzTV.Core.FFmpeg
@@ -11,14 +12,17 @@ namespace ErsatzTV.Core.FFmpeg
public class FFmpegProcessService public class FFmpegProcessService
{ {
private readonly IFFmpegStreamSelector _ffmpegStreamSelector; private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
private readonly IImageCache _imageCache;
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator; private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator;
public FFmpegProcessService( public FFmpegProcessService(
FFmpegPlaybackSettingsCalculator ffmpegPlaybackSettingsService, FFmpegPlaybackSettingsCalculator ffmpegPlaybackSettingsService,
IFFmpegStreamSelector ffmpegStreamSelector) IFFmpegStreamSelector ffmpegStreamSelector,
IImageCache imageCache)
{ {
_playbackSettingsCalculator = ffmpegPlaybackSettingsService; _playbackSettingsCalculator = ffmpegPlaybackSettingsService;
_ffmpegStreamSelector = ffmpegStreamSelector; _ffmpegStreamSelector = ffmpegStreamSelector;
_imageCache = imageCache;
} }
public async Task<Process> ForPlayoutItem( public async Task<Process> ForPlayoutItem(
@@ -46,7 +50,11 @@ namespace ErsatzTV.Core.FFmpeg
.Filter(_ => channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect) .Filter(_ => channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect)
.Filter(a => a.ArtworkKind == ArtworkKind.Logo) .Filter(a => a.ArtworkKind == ArtworkKind.Logo)
.HeadOrNone() .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; Option<ChannelWatermark> maybeWatermark = channel.Watermark;
@@ -58,7 +66,7 @@ namespace ErsatzTV.Core.FFmpeg
.WithRealtimeOutput(playbackSettings.RealtimeOutput) .WithRealtimeOutput(playbackSettings.RealtimeOutput)
.WithSeek(playbackSettings.StreamSeek) .WithSeek(playbackSettings.StreamSeek)
.WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec) .WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec)
.WithWatermark(maybeWatermark, maybeWatermarkPath, channel.FFmpegProfile.Resolution) .WithWatermark(maybeWatermark, maybeWatermarkPath, channel.FFmpegProfile.Resolution, isAnimated)
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale) .WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale)
.WithAlignedAudio(playbackSettings.AudioDuration) .WithAlignedAudio(playbackSettings.AudioDuration)
.WithNormalizeLoudness(playbackSettings.NormalizeLoudness); .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>> SaveArtworkToCache(byte[] imageBuffer, ArtworkKind artworkKind);
Task<Either<BaseError, string>> CopyArtworkToCache(string path, ArtworkKind artworkKind); Task<Either<BaseError, string>> CopyArtworkToCache(string path, ArtworkKind artworkKind);
string GetPathForImage(string fileName, ArtworkKind artworkKind, Option<int> maybeMaxHeight); 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.Images;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using LanguageExt; using LanguageExt;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
@@ -18,10 +20,17 @@ namespace ErsatzTV.Infrastructure.Images
{ {
private static readonly SHA1CryptoServiceProvider Crypto; private static readonly SHA1CryptoServiceProvider Crypto;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<ImageCache> _logger;
private readonly IMemoryCache _memoryCache;
static ImageCache() => Crypto = new SHA1CryptoServiceProvider(); 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) public async Task<Either<BaseError, byte[]>> ResizeImage(byte[] imageBuffer, int height)
{ {
@@ -120,5 +129,28 @@ namespace ErsatzTV.Infrastructure.Images
return Path.Combine(baseFolder, fileName); 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) private async Task UploadLogo(InputFileChangeEventArgs e)
{ {
var buffer = new byte[e.File.Size]; try
await e.File.OpenReadStream().ReadAsync(buffer); {
Either<BaseError, string> maybeCacheFileName = await _mediator.Send(new SaveArtworkToDisk(buffer, ArtworkKind.Logo)); var buffer = new byte[e.File.Size];
maybeCacheFileName.Match( await e.File.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024).ReadAsync(buffer);
relativeFileName => Either<BaseError, string> maybeCacheFileName = await _mediator.Send(new SaveArtworkToDisk(buffer, ArtworkKind.Logo));
{ maybeCacheFileName.Match(
_model.Logo = relativeFileName; relativeFileName =>
StateHasChanged(); {
}, _model.Logo = relativeFileName;
error => StateHasChanged();
{ },
_snackbar.Add($"Unexpected error saving channel logo: {error.Value}", Severity.Error); error =>
_logger.LogError("Unexpected error saving channel logo: {Error}", error.Value); {
}); _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);
}
} }
} }