diff --git a/CHANGELOG.md b/CHANGELOG.md index cc9069556..ba7cac100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ 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 many QSV pipeline bugs +- Fix MPEG2 video format with QSV and VAAPI acceleration +- Fix playback of content with undefined colorspace ## [0.7.3-beta] - 2023-01-25 ### Added diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs index cb84bab89..e4ec0640d 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs @@ -36,7 +36,12 @@ public class p.QsvExtraHardwareFrames = update.QsvExtraHardwareFrames; p.ResolutionId = update.ResolutionId; p.VideoFormat = update.VideoFormat; - p.BitDepth = update.BitDepth; + + // mpeg2video only supports 8-bit content + p.BitDepth = update.VideoFormat == FFmpegProfileVideoFormat.Mpeg2Video + ? FFmpegProfileBitDepth.EightBit + : update.BitDepth; + p.VideoBitrate = update.VideoBitrate; p.VideoBufferSize = update.VideoBufferSize; p.AudioFormat = update.AudioFormat; diff --git a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj index f868743ff..05ee1dcbf 100644 --- a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj +++ b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj @@ -34,16 +34,4 @@ - - - Always - - - Always - - - Always - - - diff --git a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs index 6ff541b79..b5c4c246a 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs @@ -141,9 +141,9 @@ public class FFmpegPlaybackSettingsCalculator result.PixelFormat = ffmpegProfile.BitDepth switch { - FFmpegProfileBitDepth.TenBit => new PixelFormatYuv420P10Le(), + FFmpegProfileBitDepth.TenBit when ffmpegProfile.VideoFormat != FFmpegProfileVideoFormat.Mpeg2Video + => new PixelFormatYuv420P10Le(), _ => new PixelFormatYuv420P() - // _ => new PixelFormatYuv420P10Le() }; result.AudioFormat = ffmpegProfile.AudioFormat; diff --git a/ErsatzTV.FFmpeg/Capabilities/DefaultHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/DefaultHardwareCapabilities.cs index 568425dd5..010d4056b 100644 --- a/ErsatzTV.FFmpeg/Capabilities/DefaultHardwareCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/DefaultHardwareCapabilities.cs @@ -4,7 +4,18 @@ namespace ErsatzTV.FFmpeg.Capabilities; public class DefaultHardwareCapabilities : IHardwareCapabilities { - public bool CanDecode(string videoFormat, Option videoProfile, Option maybePixelFormat) => true; + public bool CanDecode(string videoFormat, Option videoProfile, Option maybePixelFormat) + { + int bitDepth = maybePixelFormat.Map(pf => pf.BitDepth).IfNone(8); + + return (videoFormat, bitDepth) switch + { + // 10-bit h264 decoding is likely not support by any hardware + (VideoFormat.H264, 10) => false, + + _ => true + }; + } public bool CanEncode(string videoFormat, Option videoProfile, Option maybePixelFormat) { diff --git a/ErsatzTV.FFmpeg/Capabilities/VaapiHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/VaapiHardwareCapabilities.cs index 657ed6d6e..e9e594d1e 100644 --- a/ErsatzTV.FFmpeg/Capabilities/VaapiHardwareCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/VaapiHardwareCapabilities.cs @@ -107,6 +107,10 @@ public class VaapiHardwareCapabilities : IHardwareCapabilities _profileEntrypoints.Contains( new VaapiProfileEntrypoint(VaapiProfile.HevcMain, VaapiEntrypoint.Encode)), + VideoFormat.Mpeg2Video => + _profileEntrypoints.Contains( + new VaapiProfileEntrypoint(VaapiProfile.Mpeg2Main, VaapiEntrypoint.Encode)), + _ => false }; diff --git a/ErsatzTV.FFmpeg/Decoder/DecoderVaapi.cs b/ErsatzTV.FFmpeg/Decoder/DecoderVaapi.cs index 1aec3a217..e0c799c2c 100644 --- a/ErsatzTV.FFmpeg/Decoder/DecoderVaapi.cs +++ b/ErsatzTV.FFmpeg/Decoder/DecoderVaapi.cs @@ -16,7 +16,12 @@ public class DecoderVaapi : DecoderBase FrameState nextState = base.NextState(currentState); return currentState.PixelFormat.Match( - pixelFormat => nextState with { PixelFormat = new PixelFormatNv12(pixelFormat.Name) }, + pixelFormat => + { + return pixelFormat.BitDepth == 8 + ? nextState with { PixelFormat = new PixelFormatNv12(pixelFormat.Name) } + : nextState with { PixelFormat = new PixelFormatVaapi(pixelFormat.Name) }; + }, () => nextState); } } diff --git a/ErsatzTV.FFmpeg/Decoder/Qsv/DecoderHevcQsv.cs b/ErsatzTV.FFmpeg/Decoder/Qsv/DecoderHevcQsv.cs index 00be75d07..1c850ad64 100644 --- a/ErsatzTV.FFmpeg/Decoder/Qsv/DecoderHevcQsv.cs +++ b/ErsatzTV.FFmpeg/Decoder/Qsv/DecoderHevcQsv.cs @@ -1,8 +1,27 @@ -namespace ErsatzTV.FFmpeg.Decoder.Qsv; +using ErsatzTV.FFmpeg.Format; + +namespace ErsatzTV.FFmpeg.Decoder.Qsv; public class DecoderHevcQsv : DecoderBase { public override string Name => "hevc_qsv"; protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; + + public override FrameState NextState(FrameState currentState) + { + FrameState nextState = base.NextState(currentState); + + return currentState.PixelFormat.Match( + pixelFormat => + { + if (pixelFormat.BitDepth == 10) + { + return nextState with { PixelFormat = new PixelFormatP010() }; + } + + return nextState with { PixelFormat = new PixelFormatNv12(pixelFormat.Name) }; + }, + () => nextState); + } } diff --git a/ErsatzTV.FFmpeg/Decoder/Qsv/DecoderVp9Qsv.cs b/ErsatzTV.FFmpeg/Decoder/Qsv/DecoderVp9Qsv.cs index 023df2853..e08be07b0 100644 --- a/ErsatzTV.FFmpeg/Decoder/Qsv/DecoderVp9Qsv.cs +++ b/ErsatzTV.FFmpeg/Decoder/Qsv/DecoderVp9Qsv.cs @@ -1,8 +1,27 @@ -namespace ErsatzTV.FFmpeg.Decoder.Qsv; +using ErsatzTV.FFmpeg.Format; + +namespace ErsatzTV.FFmpeg.Decoder.Qsv; public class DecoderVp9Qsv : DecoderBase { public override string Name => "vp9_qsv"; protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; + + public override FrameState NextState(FrameState currentState) + { + FrameState nextState = base.NextState(currentState); + + return currentState.PixelFormat.Match( + pixelFormat => + { + if (pixelFormat.BitDepth == 10) + { + return nextState with { PixelFormat = new PixelFormatP010() }; + } + + return nextState with { PixelFormat = new PixelFormatNv12(pixelFormat.Name) }; + }, + () => nextState); + } } diff --git a/ErsatzTV.FFmpeg/Encoder/Qsv/EncoderMpeg2Qsv.cs b/ErsatzTV.FFmpeg/Encoder/Qsv/EncoderMpeg2Qsv.cs new file mode 100644 index 000000000..a0d013e5a --- /dev/null +++ b/ErsatzTV.FFmpeg/Encoder/Qsv/EncoderMpeg2Qsv.cs @@ -0,0 +1,18 @@ +using ErsatzTV.FFmpeg.Format; + +namespace ErsatzTV.FFmpeg.Encoder.Qsv; + +public class EncoderMpeg2Qsv : EncoderBase +{ + public override string Name => "mpeg2_qsv"; + public override StreamKind Kind => StreamKind.Video; + + public override IList OutputOptions => + new[] { "-c:v", "mpeg2_qsv", "-low_power", "0", "-look_ahead", "0" }; + + public override FrameState NextState(FrameState currentState) => currentState with + { + VideoFormat = VideoFormat.Mpeg2Video, + FrameDataLocation = FrameDataLocation.Hardware + }; +} diff --git a/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderMpeg2Vaapi.cs b/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderMpeg2Vaapi.cs new file mode 100644 index 000000000..8fa25a4ef --- /dev/null +++ b/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderMpeg2Vaapi.cs @@ -0,0 +1,14 @@ +using ErsatzTV.FFmpeg.Format; + +namespace ErsatzTV.FFmpeg.Encoder.Vaapi; + +public class EncoderMpeg2Vaapi : EncoderBase +{ + public override string Name => "mpeg2_vaapi"; + public override StreamKind Kind => StreamKind.Video; + public override FrameState NextState(FrameState currentState) => currentState with + { + VideoFormat = VideoFormat.Mpeg2Video + // don't change the frame data location + }; +} diff --git a/ErsatzTV.FFmpeg/Filter/ColorspaceFilter.cs b/ErsatzTV.FFmpeg/Filter/ColorspaceFilter.cs index 02cf1ce69..d0515170b 100644 --- a/ErsatzTV.FFmpeg/Filter/ColorspaceFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/ColorspaceFilter.cs @@ -65,9 +65,12 @@ public class ColorspaceFilter : BaseFilter string primaries = string.IsNullOrWhiteSpace(cp.ColorPrimaries) ? "bt709" : cp.ColorPrimaries; + string space = string.IsNullOrWhiteSpace(cp.ColorSpace) + ? "bt709" + : cp.ColorSpace; inputOverrides = - $"irange={range}:ispace={cp.ColorSpace}:itrc={transfer}:iprimaries={primaries}:"; + $"irange={range}:ispace={space}:itrc={transfer}:iprimaries={primaries}:"; } string colorspace = _desiredPixelFormat.BitDepth switch diff --git a/ErsatzTV.FFmpeg/Format/PixelFormatP010.cs b/ErsatzTV.FFmpeg/Format/PixelFormatP010.cs new file mode 100644 index 000000000..95de8741c --- /dev/null +++ b/ErsatzTV.FFmpeg/Format/PixelFormatP010.cs @@ -0,0 +1,8 @@ +namespace ErsatzTV.FFmpeg.Format; + +public class PixelFormatP010 : IPixelFormat +{ + public string Name => "p010le"; + public string FFmpegName => "p010"; + public int BitDepth => 10; +} diff --git a/ErsatzTV.FFmpeg/Format/PixelFormatVaapi.cs b/ErsatzTV.FFmpeg/Format/PixelFormatVaapi.cs new file mode 100644 index 000000000..57d84fe45 --- /dev/null +++ b/ErsatzTV.FFmpeg/Format/PixelFormatVaapi.cs @@ -0,0 +1,12 @@ +namespace ErsatzTV.FFmpeg.Format; + +public class PixelFormatVaapi : IPixelFormat +{ + public PixelFormatVaapi(string name) => Name = name; + + public string Name { get; } + + public string FFmpegName => "vaapi"; + + public int BitDepth => 8; +} diff --git a/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs index a47bbc42b..457198f74 100644 --- a/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs @@ -121,9 +121,9 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder ScaledSize = videoStream.FrameSize, PaddedSize = videoStream.FrameSize, - // consider hardware frames to be wrapped in nv12 + // consider 8-bit hardware frames to be wrapped in nv12 PixelFormat = ffmpegState.DecoderHardwareAccelerationMode == HardwareAccelerationMode.Qsv - ? videoStream.PixelFormat.Map(pf => (IPixelFormat)new PixelFormatNv12(pf.Name)) + ? videoStream.PixelFormat.Map(pf => pf.BitDepth == 8 ? new PixelFormatNv12(pf.Name) : pf) : videoStream.PixelFormat, IsAnamorphic = videoStream.IsAnamorphic, @@ -137,7 +137,10 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder { IPixelFormat pixelFormat = desiredState.PixelFormat.IfNone( context.Is10BitOutput ? new PixelFormatYuv420P10Le() : new PixelFormatYuv420P()); - desiredState = desiredState with { PixelFormat = new PixelFormatNv12(pixelFormat.Name) }; + desiredState = desiredState with + { + PixelFormat = Some(context.Is10BitOutput ? pixelFormat : new PixelFormatNv12(pixelFormat.Name)) + }; } // _logger.LogDebug("After decode: {PixelFormat}", currentState.PixelFormat); @@ -184,6 +187,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder { (HardwareAccelerationMode.Qsv, VideoFormat.Hevc) => new EncoderHevcQsv(), (HardwareAccelerationMode.Qsv, VideoFormat.H264) => new EncoderH264Qsv(), + (HardwareAccelerationMode.Qsv, VideoFormat.Mpeg2Video) => new EncoderMpeg2Qsv(), (_, _) => GetSoftwareEncoder(currentState, desiredState) }; @@ -238,11 +242,78 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder IPixelFormat formatForDownload = pixelFormat; - bool usesVppQsv = videoInputFile.FilterSteps.Any(f => f is QsvFormatFilter or ScaleQsvFilter); + bool usesVppQsv = + videoInputFile.FilterSteps.Any(f => f is QsvFormatFilter or ScaleQsvFilter or DeinterlaceQsvFilter); + + // if we have no filters, check whether we need to convert pixel format + // since qsv doesn't seem to like doing that at the encoder + if (!videoInputFile.FilterSteps.Any(f => f is not IEncoder)) + { + foreach (IPixelFormat currentPixelFormat in currentState.PixelFormat) + { + bool requiresConversion = false; + + if (currentPixelFormat is PixelFormatNv12 nv) + { + foreach (IPixelFormat pf in AvailablePixelFormats.ForPixelFormat(nv.Name, null)) + { + requiresConversion = pf.FFmpegName != format.FFmpegName; + + if (!requiresConversion) + { + currentState = currentState with { PixelFormat = Some(pf) }; + } + } + } + else + { + requiresConversion = currentPixelFormat.FFmpegName != format.FFmpegName; + } + + if (requiresConversion) + { + if (currentState.FrameDataLocation == FrameDataLocation.Hardware) + { + var filter = new QsvFormatFilter(currentPixelFormat); + result.Add(filter); + currentState = filter.NextState(currentState); + + // if we need to convert 8-bit to 10-bit, do it here + if (currentPixelFormat.BitDepth == 8 && context.Is10BitOutput) + { + var p010Filter = new QsvFormatFilter(new PixelFormatP010()); + result.Add(p010Filter); + currentState = p010Filter.NextState(currentState); + } + + usesVppQsv = true; + } + } + } + } + if (!videoStream.ColorParams.IsBt709 || usesVppQsv) { _logger.LogDebug("Adding colorspace filter"); + // force p010/nv12 if we're still in hardware + if (currentState.FrameDataLocation == FrameDataLocation.Hardware) + { + foreach (int bitDepth in currentState.PixelFormat.Map(pf => pf.BitDepth)) + { + if (bitDepth is 10 && formatForDownload is not PixelFormatYuv420P10Le) + { + formatForDownload = new PixelFormatYuv420P10Le(); + currentState = currentState with { PixelFormat = Some(formatForDownload) }; + } + else if (bitDepth is 8 && formatForDownload is not PixelFormatNv12) + { + formatForDownload = new PixelFormatNv12(formatForDownload.Name); + currentState = currentState with { PixelFormat = Some(formatForDownload) }; + } + } + } + // vpp_qsv seems to strip color info, so if we use that at all, force overriding input color info var colorspace = new ColorspaceFilter( currentState, @@ -250,15 +321,6 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder format, forceInputOverrides: usesVppQsv); - // force nv12 if we're still in hardware - if (currentState.FrameDataLocation == FrameDataLocation.Hardware) - { - if (formatForDownload is not PixelFormatNv12) - { - formatForDownload = new PixelFormatNv12(pixelFormat.Name); - } - } - currentState = colorspace.NextState(currentState); result.Add(colorspace); } @@ -295,13 +357,6 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder } } - // qsv encoders don't like yuv420p - format = format switch - { - PixelFormatYuv420P => new PixelFormatNv12(PixelFormat.YUV420P), - _ => format - }; - pipelineSteps.Add(new PixelFormatOutputOption(format)); } } @@ -375,6 +430,12 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder pf, _logger); watermarkOverlayFilterSteps.Add(watermarkFilter); + + // overlay filter with 10-bit vp9 seems to output alpha channel, so remove it with a pixel format change + if (videoStream.Codec == "vp9" && desiredPixelFormat.BitDepth == 10) + { + watermarkOverlayFilterSteps.Add(new PixelFormatFilter(new PixelFormatYuv420P10Le())); + } } } @@ -397,20 +458,9 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder { videoInputFile.AddOption(new CopyTimestampInputOption()); - // if (videoInputFile.FilterSteps.Count == 0 && videoInputFile.InputOptions.OfType().Any()) - // { - // // change the hw accel output to software so the explicit download isn't needed - // foreach (CuvidDecoder decoder in videoInputFile.InputOptions.OfType()) - // { - // decoder.HardwareAccelerationMode = HardwareAccelerationMode.None; - // } - // } - // else - // { - var downloadFilter = new HardwareDownloadFilter(currentState); - currentState = downloadFilter.NextState(currentState); - videoInputFile.FilterSteps.Add(downloadFilter); - // } + var downloadFilter = new HardwareDownloadFilter(currentState); + currentState = downloadFilter.NextState(currentState); + videoInputFile.FilterSteps.Add(downloadFilter); var subtitlesFilter = new SubtitlesFilter(fontsFolder, subtitle); currentState = subtitlesFilter.NextState(currentState); @@ -436,6 +486,12 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder var subtitlesFilter = new OverlaySubtitleFilter(pf); subtitleOverlayFilterSteps.Add(subtitlesFilter); + + // overlay filter with 10-bit vp9 seems to output alpha channel, so remove it with a pixel format change + if (videoInputFile.VideoStreams.Any(vs => vs.Codec == "vp9") && context.Is10BitOutput) + { + subtitleOverlayFilterSteps.Add(new PixelFormatFilter(new PixelFormatYuv420P10Le())); + } } } } @@ -488,11 +544,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder scaleStep = new ScaleQsvFilter( currentState with { - PixelFormat = //context.HasWatermark || - //context.HasSubtitleOverlay || - // (desiredState.ScaledSize != desiredState.PaddedSize) || - // context.HasSubtitleText || - ffmpegState is + PixelFormat = ffmpegState is { DecoderHardwareAccelerationMode: HardwareAccelerationMode.Nvenc, EncoderHardwareAccelerationMode: HardwareAccelerationMode.None diff --git a/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs index 49475fd57..fb0f6ad35 100644 --- a/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs @@ -197,6 +197,7 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder { (HardwareAccelerationMode.Vaapi, VideoFormat.Hevc) => new EncoderHevcVaapi(), (HardwareAccelerationMode.Vaapi, VideoFormat.H264) => new EncoderH264Vaapi(), + (HardwareAccelerationMode.Vaapi, VideoFormat.Mpeg2Video) => new EncoderMpeg2Vaapi(), (_, _) => GetSoftwareEncoder(currentState, desiredState) }; diff --git a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs index 7c7bb9325..8ff296843 100644 --- a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs @@ -3,6 +3,7 @@ using System.Security.Cryptography; using System.Text; using Bugsnag; using CliWrap; +using CliWrap.Buffered; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; @@ -102,41 +103,41 @@ public class TranscodingTests public static Watermark[] Watermarks = { Watermark.None, - // Watermark.PermanentOpaqueScaled, + Watermark.PermanentOpaqueScaled, // Watermark.PermanentOpaqueActualSize, - // Watermark.PermanentTransparentScaled, + Watermark.PermanentTransparentScaled, // Watermark.PermanentTransparentActualSize }; public static Subtitle[] Subtitles = { Subtitle.None, - // Subtitle.Picture, - // Subtitle.Text + Subtitle.Picture, + Subtitle.Text }; public static Padding[] Paddings = { Padding.NoPadding, - // Padding.WithPadding + Padding.WithPadding }; public static VideoScanKind[] VideoScanKinds = { VideoScanKind.Progressive, - // VideoScanKind.Interlaced + VideoScanKind.Interlaced }; public static InputFormat[] InputFormats = { // // example format that requires colorspace filter - // new("libx264", "yuv420p", "tv", "smpte170m", "bt709", "smpte170m"), + new("libx264", "yuv420p", "tv", "smpte170m", "bt709", "smpte170m"), // // // example format that requires setparams filter - // new("libx264", "yuv420p", string.Empty, string.Empty, string.Empty, string.Empty), + new("libx264", "yuv420p", string.Empty, string.Empty, string.Empty, string.Empty), // // // new("libx264", "yuvj420p"), - // new("libx264", "yuv420p10le"), + new("libx264", "yuv420p10le"), // // new("libx264", "yuv444p10le"), // // // new("mpeg1video", "yuv420p"), @@ -144,12 +145,13 @@ public class TranscodingTests // // new("mpeg2video", "yuv420p"), // new("libx265", "yuv420p"), - // new("libx265", "yuv420p10le"), + new("libx265", "yuv420p10le"), // // // new("mpeg4", "yuv420p"), // // - // // new("libvpx-vp9", "yuv420p"), - // // + new("libvpx-vp9", "yuv420p"), + new("libvpx-vp9", "yuv420p10le"), + // // // // new("libaom-av1", "yuv420p") // // // av1 yuv420p10le 51 // // @@ -162,26 +164,27 @@ public class TranscodingTests public static Resolution[] Resolutions = { new() { Width = 1920, Height = 1080 }, - // new() { Width = 1280, Height = 720 } + new() { Width = 1280, Height = 720 } }; public static FFmpegProfileBitDepth[] BitDepths = { FFmpegProfileBitDepth.EightBit, - // FFmpegProfileBitDepth.TenBit + FFmpegProfileBitDepth.TenBit }; public static FFmpegProfileVideoFormat[] VideoFormats = { FFmpegProfileVideoFormat.H264, - // FFmpegProfileVideoFormat.Hevc + FFmpegProfileVideoFormat.Hevc, + // FFmpegProfileVideoFormat.Mpeg2Video }; public static HardwareAccelerationKind[] TestAccelerations = { // HardwareAccelerationKind.None, - HardwareAccelerationKind.Nvenc, - // HardwareAccelerationKind.Vaapi, + // HardwareAccelerationKind.Nvenc, + HardwareAccelerationKind.Vaapi, // HardwareAccelerationKind.Qsv, // HardwareAccelerationKind.VideoToolbox, // HardwareAccelerationKind.Amf @@ -216,11 +219,13 @@ public class TranscodingTests string file = fileToTest; if (string.IsNullOrWhiteSpace(file)) { - if (inputFormat.Encoder is "mpeg1video" or "msmpeg4v2" or "msmpeg4v3") + // some formats don't support interlaced content (mpeg1video, msmpeg4v2, msmpeg4v3) + // others (libx265, any 10-bit) are unlikely to have interlaced content, so don't bother testing + if (inputFormat.Encoder is "mpeg1video" or "msmpeg4v2" or "msmpeg4v3" or "libx265" || inputFormat.PixelFormat.Contains("10")) { if (videoScanKind == VideoScanKind.Interlaced) { - Assert.Inconclusive($"{inputFormat.Encoder} does not support interlaced content"); + Assert.Inconclusive($"{inputFormat.Encoder}/{inputFormat.PixelFormat} does not support interlaced content"); return; } } @@ -518,7 +523,7 @@ public class TranscodingTests string.Empty, subtitleMode, now, - now + TimeSpan.FromSeconds(5), + now + TimeSpan.FromSeconds(3), now, Option.None, channelWatermark, @@ -528,7 +533,7 @@ public class TranscodingTests false, FillerKind.None, TimeSpan.Zero, - TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(3), 0, None, false, @@ -557,6 +562,12 @@ public class TranscodingTests .WithStandardOutputPipe(PipeTarget.ToFile(tempFile)) .WithStandardErrorPipe(PipeTarget.ToStringBuilder(sb)) .ExecuteAsync(timeoutSignal.Token); + + // var arguments = string.Join( + // ' ', + // process.Arguments.Split(" ").Map(a => a.Contains('[') ? $"\"{a}\"" : a)); + // + // Log.Logger.Debug(arguments); } catch (OperationCanceledException) { @@ -636,7 +647,9 @@ public class TranscodingTests videoStream.ColorPrimaries); // AMF doesn't seem to set this metadata properly - if (profileAcceleration != HardwareAccelerationKind.Amf) + // MPEG2Video doesn't always seem to set this properly + if (profileAcceleration != HardwareAccelerationKind.Amf && + profileVideoFormat != FFmpegProfileVideoFormat.Mpeg2Video) { colorParams.IsBt709.Should().BeTrue($"{colorParams}"); } @@ -709,20 +722,13 @@ public class TranscodingTests TestContext.CurrentContext.TestDirectory, "Resources", subtitle == Subtitle.Picture ? "test.sup" : "test.srt"); - var p2 = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = ExecutableName("mkvmerge"), - Arguments = - $"-o {tempFileName} {sourceFile} --field-order 0:{(videoScanKind == VideoScanKind.Interlaced ? '1' : '0')} {subPath}" - } - }; - p2.Start(); - await p2.WaitForExitAsync(); - // ReSharper disable once MethodHasAsyncOverload - p2.WaitForExit(); + BufferedCommandResult p2 = await new Command(ExecutableName("mkvmerge")) + .WithArguments( + $"-o {tempFileName} {sourceFile} --field-order 0:{(videoScanKind == VideoScanKind.Interlaced ? '1' : '0')} {subPath}") + .WithValidation(CommandResultValidation.None) + .ExecuteBufferedAsync(); + if (p2.ExitCode != 0) { if (File.Exists(sourceFile)) diff --git a/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj b/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj index 2fc64d23e..439348d57 100644 --- a/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj +++ b/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj @@ -39,6 +39,15 @@ Always + + Always + + + Always + + + Always + diff --git a/ErsatzTV.Core.Tests/Resources/ErsatzTV.png b/ErsatzTV.Scanner.Tests/Resources/ErsatzTV.png similarity index 100% rename from ErsatzTV.Core.Tests/Resources/ErsatzTV.png rename to ErsatzTV.Scanner.Tests/Resources/ErsatzTV.png diff --git a/ErsatzTV.Core.Tests/Resources/test.srt b/ErsatzTV.Scanner.Tests/Resources/test.srt similarity index 100% rename from ErsatzTV.Core.Tests/Resources/test.srt rename to ErsatzTV.Scanner.Tests/Resources/test.srt diff --git a/ErsatzTV.Core.Tests/Resources/test.sup b/ErsatzTV.Scanner.Tests/Resources/test.sup old mode 100755 new mode 100644 similarity index 100% rename from ErsatzTV.Core.Tests/Resources/test.sup rename to ErsatzTV.Scanner.Tests/Resources/test.sup diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index de6c018af..9617f4c2e 100644 --- a/ErsatzTV/ErsatzTV.csproj +++ b/ErsatzTV/ErsatzTV.csproj @@ -72,7 +72,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs b/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs index c8eb0c54f..000abc87e 100644 --- a/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs +++ b/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs @@ -90,5 +90,11 @@ public class FFmpegProfileEditViewModelValidator : AbstractValidator x.VideoFormat).Must(c => AmfFormats.Contains(c)) .WithMessage("Amf supports formats (h264, hevc)"); }); + + When( + x => x.VideoFormat == FFmpegProfileVideoFormat.Mpeg2Video, + () => RuleFor(x => x.BitDepth) + .Must(bd => bd is FFmpegProfileBitDepth.EightBit) + .WithMessage("Mpeg2Video does not support 10-bit content")); } }