From 765df64555cb0b9337f902ca2a7557c565787ecc Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Thu, 14 Apr 2022 22:30:26 -0500 Subject: [PATCH] add picture subtitle transcoding tests, and make them all pass with nvenc (#734) --- CHANGELOG.md | 2 + .../ErsatzTV.Core.Tests.csproj | 6 +- .../FFmpeg/TranscodingTests.cs | 72 ++++++++++++++++-- ErsatzTV.Core.Tests/Resources/test.sup | Bin 0 -> 1789 bytes ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs | 7 +- .../Encoder/Nvenc/EncoderH264Nvenc.cs | 18 ++++- .../Encoder/Nvenc/EncoderHevcNvenc.cs | 18 ++++- ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj | 2 +- ErsatzTV.FFmpeg/PipelineBuilder.cs | 1 + 9 files changed, 109 insertions(+), 17 deletions(-) create mode 100755 ErsatzTV.Core.Tests/Resources/test.sup diff --git a/CHANGELOG.md b/CHANGELOG.md index fbc7657d4..8e3ea8a4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Fixed +- Fix subtitles edge case with NVENC ## [0.5.0-beta] - 2022-04-13 ### Fixed diff --git a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj index b616b5f80..7f8467f2d 100644 --- a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj +++ b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj @@ -8,9 +8,10 @@ - + + @@ -36,6 +37,9 @@ Always + + Always + diff --git a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs b/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs index 3ae617778..6fa834cc4 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs @@ -65,6 +65,12 @@ public class TranscodingTests // TODO: animated vs static } + public enum Subtitle + { + None, + Picture + } + private class TestData { public static Watermark[] Watermarks = @@ -73,6 +79,12 @@ public class TranscodingTests Watermark.PermanentOpaque, Watermark.PermanentTransparent }; + + public static Subtitle[] Subtitles = + { + Subtitle.None, + Subtitle.Picture + }; public static Padding[] Paddings = { @@ -158,6 +170,7 @@ public class TranscodingTests [ValueSource(typeof(TestData), nameof(TestData.Paddings))] Padding padding, [ValueSource(typeof(TestData), nameof(TestData.VideoScanKinds))] VideoScanKind videoScanKind, [ValueSource(typeof(TestData), nameof(TestData.Watermarks))] Watermark watermark, + [ValueSource(typeof(TestData), nameof(TestData.Subtitles))] Subtitle subtitle, [ValueSource(typeof(TestData), nameof(TestData.VideoFormats))] FFmpegProfileVideoFormat profileVideoFormat, // [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration) [ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration) @@ -175,7 +188,7 @@ public class TranscodingTests } string name = GetStringSha256Hash( - $"{inputFormat.Encoder}_{inputFormat.PixelFormat}_{videoScanKind}_{padding}_{profileResolution}_{profileVideoFormat}_{profileAcceleration}"); + $"{inputFormat.Encoder}_{inputFormat.PixelFormat}_{videoScanKind}_{padding}_{watermark}_{subtitle}_{profileResolution}_{profileVideoFormat}_{profileAcceleration}"); string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv"); if (!File.Exists(file)) @@ -203,6 +216,46 @@ public class TranscodingTests // ReSharper disable once MethodHasAsyncOverload p1.WaitForExit(); p1.ExitCode.Should().Be(0); + + switch (subtitle) + { + case Subtitle.Picture: + string sourceFile = Path.GetTempFileName() + ".mkv"; + File.Move(file, sourceFile, true); + + string tempFileName = Path.GetTempFileName() + ".mkv"; + string subPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources", "test.sup"); + var p2 = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = ExecutableName("mkvmerge"), + Arguments = $"-o {tempFileName} {sourceFile} {subPath}" + } + }; + + p2.Start(); + await p2.WaitForExitAsync(); + // ReSharper disable once MethodHasAsyncOverload + p2.WaitForExit(); + if (p2.ExitCode != 0) + { + if (File.Exists(sourceFile)) + { + File.Delete(sourceFile); + } + + if (File.Exists(file)) + { + File.Delete(file); + } + } + + p2.ExitCode.Should().Be(0); + + File.Move(tempFileName, file, true); + break; + } } var imageCache = new Mock(); @@ -221,7 +274,7 @@ public class TranscodingTests imageCache.Object, new Mock().Object, new Mock().Object, - new Mock().Object, + new MemoryCache(new MemoryCacheOptions()), LoggerFactory.CreateLogger()); var service = new FFmpegLibraryProcessService( @@ -318,6 +371,12 @@ public class TranscodingTests break; } + ChannelSubtitleMode subtitleMode = subtitle switch + { + Subtitle.Picture => ChannelSubtitleMode.Any, + _ => ChannelSubtitleMode.None + }; + using Process process = await service.ForPlayoutItem( ExecutableName("ffmpeg"), ExecutableName("ffprobe"), @@ -329,9 +388,11 @@ public class TranscodingTests { HardwareAcceleration = profileAcceleration, VideoFormat = profileVideoFormat, - AudioFormat = FFmpegProfileAudioFormat.Aac + AudioFormat = FFmpegProfileAudioFormat.Aac, + DeinterlaceVideo = true }, - StreamingMode = StreamingMode.TransportStream + StreamingMode = StreamingMode.TransportStream, + SubtitleMode = subtitleMode }, v, v, @@ -368,6 +429,7 @@ public class TranscodingTests result = await Cli.Wrap(process.StartInfo.FileName) .WithArguments(process.StartInfo.ArgumentList) .WithValidation(CommandResultValidation.None) + .WithStandardOutputPipe(PipeTarget.Null) .WithStandardErrorPipe(PipeTarget.ToStringBuilder(sb)) .ExecuteAsync(timeoutSignal.Token); } @@ -425,7 +487,7 @@ public class TranscodingTests Optional(version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask(); public Task> SelectSubtitleStream(Channel channel, MediaVersion version) => - Optional(version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Subtitle)).AsTask(); + Optional(version.Streams.Find(s => s.MediaStreamKind == MediaStreamKind.Subtitle)).AsTask(); } private static string ExecutableName(string baseName) => diff --git a/ErsatzTV.Core.Tests/Resources/test.sup b/ErsatzTV.Core.Tests/Resources/test.sup new file mode 100755 index 0000000000000000000000000000000000000000..fd43361ea55d09e1bd88e2f41fbca12ff57bd46a GIT binary patch literal 1789 zcmb7_SxjS97{|ZMZEu;DmL8T{dI}U;Xek9sDNBK}Z>0kThMl3qzML2JZQis=fnkgo zqaowd#LSDDsL@1CG>&nMYceK2xIDPTWujvyCi>!*==i_wI0T{(Ht8?l_CM=)?;Rf^ zY3UJ?Mru{c&n9`HjIxqM65%I*Bb&yx{6#HqBcqfKQG_bA@JOvzRFG8wT9e6u1axHq zHW@JJfQR+>B$Kz5J;2ZoWFuB{3OH{BUN-?R=KxD);1kORN^)U6YP|-F-2(F9Ue5<| z3IK26C}QdhfbB`32(drxK#&8s9bW*&uzn~3Vx`Z(b$$qxeGim>2DlagH?kjB07aF+ zQyyT=3;a@b8Cf5!*=pckKag7kY}Eo|b-rc?OuCd5@2QH098q zFe{2Bmr#wQ(Mku*l9x}1qM%T<71?Q5a0gS1OGG+vDy3a#87l`}?g~^%7UtlU|5SN0 zRXJYTg{lhl=2ej7tCsw9LHMPd8Z0LzYBf=vB*{Vqw87Id#OrA%*Z^Iq5=LXdpeTCY zBqQP1d7BlbnHzmR4lV6)Z;J>YN%u9UDuhMKt*$oOVeOF)S;(DTC}rJN-lNh5W~u89 z_c2?4X0{_qb#pfxuni6|rbI^rtaAWnxmM47$bGU+k)0hV6L8h!O>)R>C z%B@{-U*^iv^#d#tJ~2n19%bbQ+_2qaG`7r{a|%3_;dmP>?H_S5#`B<(IQr&< zilohQW;Il9RhnIsT*rr-1Fc)Z&30B5sNvmAncgxP{lc`OxaND0&nI*WW96#4j>hF~ z&R9$xRb$NIGW*O{=1^6ut2NUpwS_8ScwHwy3r8v=2>x$uAEa-|mJ(unZ z6Q%fS37)ZqqC&+MQMfle?`tq95mnL0!wH`dO+}PaR(P~RE4_Q}a--Y5RG@8oAsh;s zqlw^#*l0CVDrBCW+s1e$d7;p1bUt4zOk`GgDE#)3ZF?_N`F6>(VaLPOiv#VYi&~5R p*IVMwy9E_V(q1&W(&zD?QOT6~fXT>Y;!h(!cJjl8Qmr6K^f!#9$m0M2 literal 0 HcmV?d00001 diff --git a/ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs b/ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs index 2d1403432..c00fb9d1b 100644 --- a/ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs +++ b/ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs @@ -15,15 +15,18 @@ public static class AvailableEncoders FrameState currentState, FrameState desiredState, Option maybeWatermarkInputFile, + Option maybeSubtitleInputFile, ILogger logger) => (ffmpegState.HardwareAccelerationMode, desiredState.VideoFormat) switch { (HardwareAccelerationMode.Nvenc, VideoFormat.Hevc) => new EncoderHevcNvenc( currentState, - maybeWatermarkInputFile), + maybeWatermarkInputFile, + maybeSubtitleInputFile), (HardwareAccelerationMode.Nvenc, VideoFormat.H264) => new EncoderH264Nvenc( currentState, - maybeWatermarkInputFile), + maybeWatermarkInputFile, + maybeSubtitleInputFile), (HardwareAccelerationMode.Qsv, VideoFormat.Hevc) => new EncoderHevcQsv( currentState, diff --git a/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderH264Nvenc.cs b/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderH264Nvenc.cs index f437145d2..9e2303da1 100644 --- a/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderH264Nvenc.cs +++ b/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderH264Nvenc.cs @@ -6,11 +6,16 @@ public class EncoderH264Nvenc : EncoderBase { private readonly FrameState _currentState; private readonly Option _maybeWatermarkInputFile; + private readonly Option _maybeSubtitleInputFile; - public EncoderH264Nvenc(FrameState currentState, Option maybeWatermarkInputFile) + public EncoderH264Nvenc( + FrameState currentState, + Option maybeWatermarkInputFile, + Option maybeSubtitleInputFile) { _currentState = currentState; _maybeWatermarkInputFile = maybeWatermarkInputFile; + _maybeSubtitleInputFile = maybeSubtitleInputFile; } public override FrameState NextState(FrameState currentState) => currentState with @@ -27,10 +32,15 @@ public class EncoderH264Nvenc : EncoderBase { get { - // only upload to hw if we need to overlay a watermark - if (_maybeWatermarkInputFile.IsSome && _currentState.FrameDataLocation == FrameDataLocation.Software) + // only upload to hw if we need to overlay (watermark or subtitle) + if (_currentState.FrameDataLocation == FrameDataLocation.Software) { - return "hwupload_cuda"; + bool isPictureSubtitle = _maybeSubtitleInputFile.Map(s => s.IsImageBased).IfNone(false); + + if (isPictureSubtitle || _maybeWatermarkInputFile.IsSome) + { + return "hwupload_cuda"; + } } return string.Empty; diff --git a/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderHevcNvenc.cs b/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderHevcNvenc.cs index 66549579b..225d41752 100644 --- a/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderHevcNvenc.cs +++ b/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderHevcNvenc.cs @@ -6,11 +6,16 @@ public class EncoderHevcNvenc : EncoderBase { private readonly FrameState _currentState; private readonly Option _maybeWatermarkInputFile; + private readonly Option _maybeSubtitleInputFile; - public EncoderHevcNvenc(FrameState currentState, Option maybeWatermarkInputFile) + public EncoderHevcNvenc( + FrameState currentState, + Option maybeWatermarkInputFile, + Option maybeSubtitleInputFile) { _currentState = currentState; _maybeWatermarkInputFile = maybeWatermarkInputFile; + _maybeSubtitleInputFile = maybeSubtitleInputFile; } public override FrameState NextState(FrameState currentState) => currentState with @@ -27,10 +32,15 @@ public class EncoderHevcNvenc : EncoderBase { get { - // only upload to hw if we need to overlay a watermark - if (_maybeWatermarkInputFile.IsSome && _currentState.FrameDataLocation == FrameDataLocation.Software) + // only upload to hw if we need to overlay (watermark or subtitle) + if (_currentState.FrameDataLocation == FrameDataLocation.Software) { - return "hwupload_cuda"; + bool isPictureSubtitle = _maybeSubtitleInputFile.Map(s => s.IsImageBased).IfNone(false); + + if (isPictureSubtitle || _maybeWatermarkInputFile.IsSome) + { + return "hwupload_cuda"; + } } return string.Empty; diff --git a/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj b/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj index 4dd4199f3..2179482af 100644 --- a/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj +++ b/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj @@ -7,7 +7,7 @@ - + diff --git a/ErsatzTV.FFmpeg/PipelineBuilder.cs b/ErsatzTV.FFmpeg/PipelineBuilder.cs index 5737069d2..92323ba8c 100644 --- a/ErsatzTV.FFmpeg/PipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/PipelineBuilder.cs @@ -474,6 +474,7 @@ public class PipelineBuilder currentState, desiredState, _watermarkInputFile, + _subtitleInputFile, _logger)) { encoder = e;