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 000000000..fd43361ea Binary files /dev/null and b/ErsatzTV.Core.Tests/Resources/test.sup differ 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;