Browse Source

allow animated channel watermarks (#255)

pull/256/head
Jason Dove 5 years ago committed by GitHub
parent
commit
0750a0712f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  2. 14
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  3. 1
      ErsatzTV.Core/Interfaces/Images/IImageCache.cs
  4. 34
      ErsatzTV.Infrastructure/Images/ImageCache.cs
  5. 41
      ErsatzTV/Pages/ChannelEditor.razor

12
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -155,15 +155,19 @@ namespace ErsatzTV.Core.FFmpeg @@ -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);
}

14
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -4,6 +4,7 @@ using System.IO; @@ -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 @@ -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 @@ -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 @@ -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);

1
ErsatzTV.Core/Interfaces/Images/IImageCache.cs

@ -10,5 +10,6 @@ namespace ErsatzTV.Core.Interfaces.Images @@ -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);
}
}

34
ErsatzTV.Infrastructure/Images/ImageCache.cs

@ -8,6 +8,8 @@ using ErsatzTV.Core.Domain; @@ -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 @@ -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 @@ -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;
}
}
}
}

41
ErsatzTV/Pages/ChannelEditor.razor

@ -248,20 +248,33 @@ @@ -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);
}
}
}
Loading…
Cancel
Save