From a90eb2d4debc17a7e4ced87e4e141e3cb21ee673 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Thu, 25 Nov 2021 12:31:57 -0600 Subject: [PATCH] optimize generated video (#498) * use different framerate flags * pre-generate song image and always use software encoders * fix tests --- .../GetConcatProcessByChannelNumberHandler.cs | 6 +- ...layoutItemProcessByChannelNumberHandler.cs | 56 ++++++++--- .../FFmpeg/FFmpegComplexFilterBuilderTests.cs | 18 ++-- .../FFmpeg/TranscodingTests.cs | 3 +- .../MediaItem/BackgroundImageMediaVersion.cs | 6 ++ .../FFmpeg/FFmpegComplexFilterBuilder.cs | 38 +++----- ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs | 47 +++++++-- ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs | 97 ++++++++++++++++++- .../FFmpeg/IFFmpegProcessService.cs | 51 ++++++++++ ErsatzTV.Core/Metadata/LocalFolderScanner.cs | 6 +- ErsatzTV.Core/Metadata/MovieFolderScanner.cs | 4 +- .../Metadata/MusicVideoFolderScanner.cs | 4 +- .../Metadata/OtherVideoFolderScanner.cs | 4 +- ErsatzTV.Core/Metadata/SongFolderScanner.cs | 4 +- .../Metadata/TelevisionFolderScanner.cs | 4 +- ErsatzTV/Startup.cs | 2 +- 16 files changed, 272 insertions(+), 78 deletions(-) create mode 100644 ErsatzTV.Core/Domain/MediaItem/BackgroundImageMediaVersion.cs create mode 100644 ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs diff --git a/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs index 8da833d19..4a673abb5 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using ErsatzTV.Core.FFmpeg; +using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Runtime; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; @@ -15,12 +15,12 @@ namespace ErsatzTV.Application.Streaming.Queries { public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler { - private readonly FFmpegProcessService _ffmpegProcessService; + private readonly IFFmpegProcessService _ffmpegProcessService; private readonly IRuntimeInfo _runtimeInfo; public GetConcatProcessByChannelNumberHandler( IDbContextFactory dbContextFactory, - FFmpegProcessService ffmpegProcessService, + IFFmpegProcessService ffmpegProcessService, IRuntimeInfo runtimeInfo) : base(dbContextFactory) { diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs index ed1e4e205..bc5036bff 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs @@ -11,8 +11,8 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Errors; using ErsatzTV.Core.Extensions; -using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.Emby; +using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Metadata; @@ -35,7 +35,7 @@ namespace ErsatzTV.Application.Streaming.Queries private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly ITelevisionRepository _televisionRepository; private readonly IArtistRepository _artistRepository; - private readonly FFmpegProcessService _ffmpegProcessService; + private readonly IFFmpegProcessService _ffmpegProcessService; private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService; private readonly ILocalFileSystem _localFileSystem; private readonly IPlexPathReplacementService _plexPathReplacementService; @@ -44,7 +44,7 @@ namespace ErsatzTV.Application.Streaming.Queries public GetPlayoutItemProcessByChannelNumberHandler( IDbContextFactory dbContextFactory, - FFmpegProcessService ffmpegProcessService, + IFFmpegProcessService ffmpegProcessService, ILocalFileSystem localFileSystem, IPlexPathReplacementService plexPathReplacementService, IJellyfinPathReplacementService jellyfinPathReplacementService, @@ -122,8 +122,6 @@ namespace ErsatzTV.Application.Streaming.Queries return await maybePlayoutItem.Match( async playoutItemWithPath => { - Option drawtextFile = None; - MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem.GetHeadVersion(); string videoPath = playoutItemWithPath.Path; @@ -131,9 +129,17 @@ namespace ErsatzTV.Application.Streaming.Queries string audioPath = playoutItemWithPath.Path; MediaVersion audioVersion = version; - + + Option maybeGlobalWatermark = await dbContext.ConfigElements + .GetValue(ConfigElementKey.FFmpegGlobalWatermarkId) + .BindT( + watermarkId => dbContext.ChannelWatermarks + .SelectOneAsync(w => w.Id, w => w.Id == watermarkId)); + if (playoutItemWithPath.PlayoutItem.MediaItem is Song song) { + Option drawtextFile = None; + videoVersion = new FallbackMediaVersion { Id = -1, @@ -206,18 +212,41 @@ namespace ErsatzTV.Application.Streaming.Queries { new() { Path = videoPath } }; + + Either maybeSongImage = await _ffmpegProcessService.GenerateSongImage( + ffmpegPath, + drawtextFile, + channel, + maybeGlobalWatermark, + videoVersion, + videoPath); + + foreach (string si in maybeSongImage.RightToSeq()) + { + videoPath = si; + videoVersion = new BackgroundImageMediaVersion + { + Chapters = new List(), + // song image has been pre-generated with correct size + Height = channel.FFmpegProfile.Resolution.Height, + Width = channel.FFmpegProfile.Resolution.Width, + SampleAspectRatio = "1:1", + Streams = new List + { + new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 }, + }, + MediaFiles = new List + { + new() { Path = si } + } + }; + } } bool saveReports = !_runtimeInfo.IsOSPlatform(OSPlatform.Windows) && await dbContext.ConfigElements .GetValue(ConfigElementKey.FFmpegSaveReports) .Map(result => result.IfNone(false)); - Option maybeGlobalWatermark = await dbContext.ConfigElements - .GetValue(ConfigElementKey.FFmpegGlobalWatermarkId) - .BindT( - watermarkId => dbContext.ChannelWatermarks - .SelectOneAsync(w => w.Id, w => w.Id == watermarkId)); - Process process = await _ffmpegProcessService.ForPlayoutItem( ffmpegPath, saveReports, @@ -235,8 +264,7 @@ namespace ErsatzTV.Application.Streaming.Queries request.HlsRealtime, playoutItemWithPath.PlayoutItem.FillerKind, playoutItemWithPath.PlayoutItem.InPoint, - playoutItemWithPath.PlayoutItem.OutPoint, - drawtextFile); + playoutItemWithPath.PlayoutItem.OutPoint); var result = new PlayoutItemProcessModel(process, playoutItemWithPath.PlayoutItem.FinishOffset); diff --git a/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs b/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs index 7f540aea2..2915ee39f 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs @@ -19,7 +19,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg { var builder = new FFmpegComplexFilterBuilder(); - Option result = builder.Build("", 0, 0, 0, 1, false); + Option result = builder.Build(false, 0, 0, 0, 1, false); result.IsNone.Should().BeTrue(); } @@ -31,7 +31,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() .WithAlignedAudio(duration); - Option result = builder.Build("", 0, 0, 0, 1, false); + Option result = builder.Build(false, 0, 0, 0, 1, false); result.IsSome.Should().BeTrue(); result.IfSome( @@ -52,7 +52,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() .WithAlignedAudio(duration); - Option result = builder.Build("", 0, 0, 0, 1, false); + Option result = builder.Build(false, 0, 0, 0, 1, false); result.IsSome.Should().BeTrue(); result.IfSome( @@ -72,7 +72,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg .WithAlignedAudio(duration) .WithDeinterlace(true); - Option result = builder.Build("", 0, 0, 0, 1, false); + Option result = builder.Build(false, 0, 0, 0, 1, false); result.IsSome.Should().BeTrue(); result.IfSome( @@ -123,7 +123,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); } - Option result = builder.Build("", 0, 0, 0, 1, false); + Option result = builder.Build(false, 0, 0, 0, 1, false); result.IsSome.Should().BeTrue(); result.IfSome( @@ -279,7 +279,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg .WithDeinterlace(deinterlace) .WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None); - Option result = builder.Build("", 0, 0, 0, 1, false); + Option result = builder.Build(false, 0, 0, 0, 1, false); result.IsSome.Should().BeTrue(); result.IfSome( @@ -350,7 +350,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); } - Option result = builder.Build("", 0, 0, 0, 1, false); + Option result = builder.Build(false, 0, 0, 0, 1, false); result.IsSome.Should().BeTrue(); result.IfSome( @@ -421,7 +421,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); } - Option result = builder.Build("", 0, 0, 0, 1, false); + Option result = builder.Build(false, 0, 0, 0, 1, false); result.IsSome.Should().BeTrue(); result.IfSome( @@ -543,7 +543,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); } - Option result = builder.Build("", 0, 0, 0, 1, false); + Option result = builder.Build(false, 0, 0, 0, 1, false); result.IsSome.Should().BeTrue(); result.IfSome( diff --git a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs b/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs index 30fce8f12..d886e7b79 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs @@ -196,8 +196,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg false, FillerKind.None, TimeSpan.Zero, - TimeSpan.FromSeconds(5), - None); + TimeSpan.FromSeconds(5)); process.StartInfo.RedirectStandardError = true; diff --git a/ErsatzTV.Core/Domain/MediaItem/BackgroundImageMediaVersion.cs b/ErsatzTV.Core/Domain/MediaItem/BackgroundImageMediaVersion.cs new file mode 100644 index 000000000..480cfdd04 --- /dev/null +++ b/ErsatzTV.Core/Domain/MediaItem/BackgroundImageMediaVersion.cs @@ -0,0 +1,6 @@ +namespace ErsatzTV.Core.Domain +{ + public class BackgroundImageMediaVersion : MediaVersion + { + } +} diff --git a/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs b/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs index f3a9cbdbd..c52875560 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs @@ -133,7 +133,7 @@ namespace ErsatzTV.Core.FFmpeg return this; } - public Option Build(string videoPath, int videoInput, int videoStreamIndex, int audioInput, Option audioStreamIndex, bool isSong) + public Option Build(bool videoOnly, int videoInput, int videoStreamIndex, int audioInput, Option audioStreamIndex, bool isSong) { var complexFilter = new StringBuilder(); @@ -143,9 +143,9 @@ namespace ErsatzTV.Core.FFmpeg HardwareAccelerationKind acceleration = _hardwareAccelerationKind.IfNone(HardwareAccelerationKind.None); bool isHardwareDecode = acceleration switch { - HardwareAccelerationKind.Vaapi => !isSong && _inputCodec != "mpeg4", - HardwareAccelerationKind.Nvenc => !isSong, - HardwareAccelerationKind.Qsv => !isSong, + HardwareAccelerationKind.Vaapi => _inputCodec != "mpeg4", + HardwareAccelerationKind.Nvenc => true, + HardwareAccelerationKind.Qsv => true, _ => false }; @@ -169,19 +169,6 @@ namespace ErsatzTV.Core.FFmpeg bool usesHardwareFilters = acceleration != HardwareAccelerationKind.None && !isHardwareDecode && (_deinterlace || _scaleToSize.IsSome); - if (isSong) - { - switch (acceleration) - { - case HardwareAccelerationKind.Qsv: - videoFilterQueue.Add("format=nv12"); - break; - default: - videoFilterQueue.Add("format=yuv420p"); - break; - } - } - switch (usesHardwareFilters, false, acceleration) { case (true, false, HardwareAccelerationKind.Nvenc): @@ -233,7 +220,6 @@ namespace ErsatzTV.Core.FFmpeg HardwareAccelerationKind.Qsv => $"scale_qsv=w={size.Width}:h={size.Height}", HardwareAccelerationKind.Nvenc when _pixelFormat is "yuv420p10le" => $"hwupload_cuda,scale_cuda={size.Width}:{size.Height}", - HardwareAccelerationKind.Nvenc when isSong => $"scale_cuda={size.Width}:{size.Height}:format=yuv420p", HardwareAccelerationKind.Nvenc => $"scale_cuda={size.Width}:{size.Height}", HardwareAccelerationKind.Vaapi => $"scale_vaapi=format=nv12:w={size.Width}:h={size.Height}", _ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear" @@ -258,8 +244,6 @@ namespace ErsatzTV.Core.FFmpeg HardwareAccelerationKind.Vaapi => "format=nv12|vaapi", HardwareAccelerationKind.Nvenc when _pixelFormat == "yuv420p10le" => "format=p010le,format=nv12", - HardwareAccelerationKind.Qsv when isSong => "format=nv12,format=yuv420p", - _ when isSong => "format=yuv420p", _ => "format=nv12" }; videoFilterQueue.Add(format); @@ -270,9 +254,14 @@ namespace ErsatzTV.Core.FFmpeg videoFilterQueue.Add("setsar=1"); } + if (videoOnly) + { + videoFilterQueue.Add("boxblur=40"); + } + if (isSong) { - videoFilterQueue.Add("boxblur=75,fps=24"); + videoFilterQueue.Add("fps=30"); } foreach (ChannelWatermark watermark in _watermark) @@ -408,12 +397,9 @@ namespace ErsatzTV.Core.FFmpeg if (usesSoftwareFilters && acceleration != HardwareAccelerationKind.None) { - switch (isSong, acceleration) + switch (acceleration) { - case (true, HardwareAccelerationKind.Nvenc): - complexFilter.Append(",hwupload_cuda"); - break; - case (_, HardwareAccelerationKind.Qsv): + case HardwareAccelerationKind.Qsv: complexFilter.Append(",format=yuv420p,hwupload=extra_hw_frames=64"); break; default: diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs index 9be9858b9..26d5e344e 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs @@ -43,6 +43,7 @@ namespace ErsatzTV.Core.FFmpeg private HardwareAccelerationKind _hwAccel; private string _outputPixelFormat; private bool _noAutoScale; + private Option _outputFramerate; public FFmpegProcessBuilder(string ffmpegPath, bool saveReports, ILogger logger) { @@ -248,6 +249,7 @@ namespace ErsatzTV.Core.FFmpeg else { _noAutoScale = true; + _outputFramerate = 30; _arguments.Add("-loop"); _arguments.Add("1"); @@ -276,6 +278,24 @@ namespace ErsatzTV.Core.FFmpeg return this; } + + public FFmpegProcessBuilder WithSongInput( + string videoPath, + Option codec, + Option pixelFormat) + { + _noAutoScale = true; + _outputFramerate = 30; + + _complexFilterBuilder = _complexFilterBuilder + .WithInputCodec(codec) + .WithInputPixelFormat(pixelFormat); + + _arguments.Add("-i"); + _arguments.Add(videoPath); + + return this; + } public FFmpegProcessBuilder WithFiltergraph(string graph) { @@ -486,6 +506,12 @@ namespace ErsatzTV.Core.FFmpeg _arguments.Add("-noautoscale"); } + foreach (int framerate in _outputFramerate) + { + _arguments.Add("-r"); + _arguments.Add(framerate.ToString()); + } + return this; } @@ -545,7 +571,7 @@ namespace ErsatzTV.Core.FFmpeg MediaStream videoStream, Option maybeAudioStream, string videoPath, - string audioPath, + Option audioPath, string videoCodec) { _complexFilterBuilder = _complexFilterBuilder.WithVideoEncoder(videoCodec); @@ -555,21 +581,27 @@ namespace ErsatzTV.Core.FFmpeg var videoIndex = 0; var audioIndex = 0; - if (audioPath != videoPath) + if (audioPath.IsNone) + { + // no audio index, so use same as video + audioIndex = 0; + } + else if (audioPath.IfNone("NotARealPath") != videoPath) { audioIndex = 1; + _outputPixelFormat = "yuv420p"; } var videoLabel = $"{videoIndex}:{videoStreamIndex}"; var audioLabel = $"{audioIndex}:{maybeIndex.Match(i => i.ToString(), () => "a")}"; Option maybeFilter = _complexFilterBuilder.Build( - videoPath, + audioPath.IsNone, videoIndex, videoStreamIndex, audioIndex, maybeIndex, - videoPath != audioPath); + audioPath.IsSome && videoPath != audioPath.IfNone("NotARealPath")); maybeFilter.IfSome( filter => @@ -588,8 +620,11 @@ namespace ErsatzTV.Core.FFmpeg _arguments.Add("-map"); _arguments.Add(videoLabel); - _arguments.Add("-map"); - _arguments.Add(audioLabel); + foreach (string _ in audioPath) + { + _arguments.Add("-map"); + _arguments.Add(audioLabel); + } return this; } diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs index a978e713e..c80db7ad7 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs @@ -12,7 +12,7 @@ using static LanguageExt.Prelude; namespace ErsatzTV.Core.FFmpeg { - public class FFmpegProcessService + public class FFmpegProcessService : IFFmpegProcessService { private readonly IFFmpegStreamSelector _ffmpegStreamSelector; private readonly IImageCache _imageCache; @@ -48,8 +48,7 @@ namespace ErsatzTV.Core.FFmpeg bool hlsRealtime, FillerKind fillerKind, TimeSpan inPoint, - TimeSpan outPoint, - Option drawtextFile) + TimeSpan outPoint) { MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion); Option maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion); @@ -65,6 +64,15 @@ namespace ErsatzTV.Core.FFmpeg inPoint, outPoint); + if (videoVersion is BackgroundImageMediaVersion) + { + FFmpegPlaybackSettings errorSettings = + _playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile); + + playbackSettings.HardwareAcceleration = errorSettings.HardwareAcceleration; + playbackSettings.VideoCodec = errorSettings.VideoCodec; + } + Option watermarkOptions = await GetWatermarkOptions(channel, globalWatermark, videoVersion); @@ -87,7 +95,6 @@ namespace ErsatzTV.Core.FFmpeg videoStream.Codec, videoStream.PixelFormat) .WithWatermark(watermarkOptions, channel.FFmpegProfile.Resolution) - .WithDrawtextFile(videoVersion, drawtextFile) .WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale) .WithAlignedAudio(videoPath == audioPath ? playbackSettings.AudioDuration : Option.None) .WithNormalizeLoudness(playbackSettings.NormalizeLoudness); @@ -234,6 +241,83 @@ namespace ErsatzTV.Core.FFmpeg .Build(); } + public async Task> GenerateSongImage( + string ffmpegPath, + Option drawtextFile, + Channel channel, + Option globalWatermark, + MediaVersion videoVersion, + string videoPath) + { + try + { + // todo: generate name by channel? clean up old images? + string outputFile = Path.GetTempFileName(); + + MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion); + + Option watermarkOptions = + await GetWatermarkOptions(channel, globalWatermark, videoVersion); + + FFmpegPlaybackSettings playbackSettings = + _playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile); + + FFmpegPlaybackSettings scalePlaybackSettings = _playbackSettingsCalculator.CalculateSettings( + StreamingMode.TransportStream, + channel.FFmpegProfile, + videoVersion, + videoStream, + None, + DateTimeOffset.UnixEpoch, + DateTimeOffset.UnixEpoch, + TimeSpan.Zero, + TimeSpan.Zero); + + FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, false, _logger) + .WithThreads(1) + .WithQuiet() + .WithFormatFlags(playbackSettings.FormatFlags) + .WithRealtimeOutput(playbackSettings.RealtimeOutput) + .WithSongInput(videoPath, videoStream.Codec, videoStream.PixelFormat) + .WithWatermark(watermarkOptions, channel.FFmpegProfile.Resolution) + .WithDrawtextFile(videoVersion, drawtextFile); + + foreach (IDisplaySize scaledSize in scalePlaybackSettings.ScaledSize) + { + builder = builder.WithScaling(scaledSize); + + if (NeedToPad(channel.FFmpegProfile.Resolution, scaledSize)) + { + builder = builder.WithBlackBars(channel.FFmpegProfile.Resolution); + } + } + + using Process process = builder + .WithFilterComplex( + videoStream, + None, + videoPath, + None, + playbackSettings.VideoCodec) + .WithOutputFormat("apng", outputFile) + .Build(); + + _logger.LogInformation( + "ffmpeg song arguments {FFmpegArguments}", + string.Join(" ", process.StartInfo.ArgumentList)); + + process.Start(); + await process.WaitForExitAsync(); + + return outputFile; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error generating song image"); + return Left(BaseError.New(ex.Message)); + } + } + private bool NeedToPad(IDisplaySize target, IDisplaySize displaySize) => displaySize.Width != target.Width || displaySize.Height != target.Height; @@ -242,6 +326,11 @@ namespace ErsatzTV.Core.FFmpeg Option globalWatermark, MediaVersion videoVersion) { + if (videoVersion is BackgroundImageMediaVersion) + { + return new WatermarkOptions(None, None, None, false); + } + Option watermarkOverride = videoVersion is FallbackMediaVersion or CoverArtMediaVersion ? new ChannelWatermark { diff --git a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs new file mode 100644 index 000000000..bca5e5b78 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs @@ -0,0 +1,51 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain.Filler; +using ErsatzTV.Core.FFmpeg; +using LanguageExt; + +namespace ErsatzTV.Core.Interfaces.FFmpeg +{ + public interface IFFmpegProcessService + { + Task ForPlayoutItem( + string ffmpegPath, + bool saveReports, + Channel channel, + MediaVersion videoVersion, + MediaVersion audioVersion, + string videoPath, + string audioPath, + DateTimeOffset start, + DateTimeOffset finish, + DateTimeOffset now, + Option globalWatermark, + VaapiDriver vaapiDriver, + string vaapiDevice, + bool hlsRealtime, + FillerKind fillerKind, + TimeSpan inPoint, + TimeSpan outPoint); + + Process ForError( + string ffmpegPath, + Channel channel, + Option duration, + string errorMessage, + bool hlsRealtime); + + Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host); + + Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile); + + Task> GenerateSongImage( + string ffmpegPath, + Option drawtextFile, + Channel channel, + Option globalWatermark, + MediaVersion videoVersion, + string videoPath); + } +} diff --git a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs index e27c5663e..27e76fbca 100644 --- a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Extensions; -using ErsatzTV.Core.FFmpeg; +using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; @@ -49,7 +49,7 @@ namespace ErsatzTV.Core.Metadata .ToList(); private readonly IImageCache _imageCache; - private readonly FFmpegProcessService _ffmpegProcessService; + private readonly IFFmpegProcessService _ffmpegProcessService; private readonly ILocalFileSystem _localFileSystem; private readonly ILocalStatisticsProvider _localStatisticsProvider; @@ -61,7 +61,7 @@ namespace ErsatzTV.Core.Metadata ILocalStatisticsProvider localStatisticsProvider, IMetadataRepository metadataRepository, IImageCache imageCache, - FFmpegProcessService ffmpegProcessService, + IFFmpegProcessService ffmpegProcessService, ILogger logger) { _localFileSystem = localFileSystem; diff --git a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs index 7b448ae16..7fe05fec3 100644 --- a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; -using ErsatzTV.Core.FFmpeg; +using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; @@ -41,7 +41,7 @@ namespace ErsatzTV.Core.Metadata ISearchRepository searchRepository, ILibraryRepository libraryRepository, IMediator mediator, - FFmpegProcessService ffmpegProcessService, + IFFmpegProcessService ffmpegProcessService, ILogger logger) : base(localFileSystem, localStatisticsProvider, metadataRepository, imageCache, ffmpegProcessService, logger) { diff --git a/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs b/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs index 2384ead12..894c77146 100644 --- a/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; -using ErsatzTV.Core.FFmpeg; +using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; @@ -42,7 +42,7 @@ namespace ErsatzTV.Core.Metadata IMusicVideoRepository musicVideoRepository, ILibraryRepository libraryRepository, IMediator mediator, - FFmpegProcessService ffmpegProcessService, + IFFmpegProcessService ffmpegProcessService, ILogger logger) : base( localFileSystem, localStatisticsProvider, diff --git a/ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs b/ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs index 1af48f367..c58325dbc 100644 --- a/ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; -using ErsatzTV.Core.FFmpeg; +using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; @@ -40,7 +40,7 @@ namespace ErsatzTV.Core.Metadata ISearchRepository searchRepository, IOtherVideoRepository otherVideoRepository, ILibraryRepository libraryRepository, - FFmpegProcessService ffmpegProcessService, + IFFmpegProcessService ffmpegProcessService, ILogger logger) : base( localFileSystem, localStatisticsProvider, diff --git a/ErsatzTV.Core/Metadata/SongFolderScanner.cs b/ErsatzTV.Core/Metadata/SongFolderScanner.cs index 2bc3e2f62..4de5cfa6b 100644 --- a/ErsatzTV.Core/Metadata/SongFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/SongFolderScanner.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; using ErsatzTV.Core.Extensions; -using ErsatzTV.Core.FFmpeg; +using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; @@ -41,7 +41,7 @@ namespace ErsatzTV.Core.Metadata ISearchRepository searchRepository, ISongRepository songRepository, ILibraryRepository libraryRepository, - FFmpegProcessService ffmpegProcessService, + IFFmpegProcessService ffmpegProcessService, ILogger logger) : base( localFileSystem, localStatisticsProvider, diff --git a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs index 7c6b86748..09858cb89 100644 --- a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; -using ErsatzTV.Core.FFmpeg; +using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; @@ -41,7 +41,7 @@ namespace ErsatzTV.Core.Metadata ISearchRepository searchRepository, ILibraryRepository libraryRepository, IMediator mediator, - FFmpegProcessService ffmpegProcessService, + IFFmpegProcessService ffmpegProcessService, ILogger logger) : base( localFileSystem, localStatisticsProvider, diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 3bd745221..f09028dbb 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -304,7 +304,7 @@ namespace ErsatzTV services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(