allow animated channel watermarks (#255)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user