diff options
Diffstat (limited to 'MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs')
| -rw-r--r-- | MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 1181 |
1 files changed, 696 insertions, 485 deletions
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 24cd141dc..c120e08fa 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -37,6 +37,8 @@ namespace MediaBrowser.Controller.MediaEncoding /// </summary> public const string ValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; + private const string _defaultMjpegEncoder = "mjpeg"; + private const string QsvAlias = "qs"; private const string VaapiAlias = "va"; private const string D3d11vaAlias = "dx11"; @@ -69,11 +71,14 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegAdvancedTonemapMode = new Version(7, 0, 1); private readonly Version _minFFmpegAlteredVaVkInterop = new Version(7, 0, 1); private readonly Version _minFFmpegQsvVppTonemapOption = new Version(7, 0, 1); + private readonly Version _minFFmpegQsvVppOutRangeOption = new Version(7, 0, 1); + private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1); + private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0); private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled); - private static readonly string[] _videoProfilesH264 = new[] - { + private static readonly string[] _videoProfilesH264 = + [ "ConstrainedBaseline", "Baseline", "Extended", @@ -82,20 +87,20 @@ namespace MediaBrowser.Controller.MediaEncoding "ProgressiveHigh", "ConstrainedHigh", "High10" - }; + ]; - private static readonly string[] _videoProfilesH265 = new[] - { + private static readonly string[] _videoProfilesH265 = + [ "Main", "Main10" - }; + ]; - private static readonly string[] _videoProfilesAv1 = new[] - { + private static readonly string[] _videoProfilesAv1 = + [ "Main", "High", "Professional", - }; + ]; private static readonly HashSet<string> _mp4ContainerNames = new(StringComparer.OrdinalIgnoreCase) { @@ -107,8 +112,8 @@ namespace MediaBrowser.Controller.MediaEncoding "m4v", }; - private static readonly string[] _legacyTonemapModes = new[] { "max", "rgb" }; - private static readonly string[] _advancedTonemapModes = new[] { "lum", "itp" }; + private static readonly TonemappingMode[] _legacyTonemapModes = [TonemappingMode.max, TonemappingMode.rgb]; + private static readonly TonemappingMode[] _advancedTonemapModes = [TonemappingMode.lum, TonemappingMode.itp]; // Set max transcoding channels for encoders that can't handle more than a set amount of channels // AAC, FLAC, ALAC, libopus, libvorbis encoders all support at least 8 channels @@ -123,23 +128,23 @@ namespace MediaBrowser.Controller.MediaEncoding { "truehd", 6 }, }; - private static readonly string _defaultMjpegEncoder = "mjpeg"; - private static readonly Dictionary<string, string> _mjpegCodecMap = new(StringComparer.OrdinalIgnoreCase) + private static readonly Dictionary<HardwareAccelerationType, string> _mjpegCodecMap = new() { - { "vaapi", _defaultMjpegEncoder + "_vaapi" }, - { "qsv", _defaultMjpegEncoder + "_qsv" }, - { "videotoolbox", _defaultMjpegEncoder + "_videotoolbox" } + { HardwareAccelerationType.vaapi, _defaultMjpegEncoder + "_vaapi" }, + { HardwareAccelerationType.qsv, _defaultMjpegEncoder + "_qsv" }, + { HardwareAccelerationType.videotoolbox, _defaultMjpegEncoder + "_videotoolbox" }, + { HardwareAccelerationType.rkmpp, _defaultMjpegEncoder + "_rkmpp" } }; - public static readonly string[] LosslessAudioCodecs = new string[] - { + public static readonly string[] LosslessAudioCodecs = + [ "alac", "ape", "flac", "mlp", "truehd", "wavpack" - }; + ]; public EncodingHelper( IApplicationPaths appPaths, @@ -176,18 +181,18 @@ namespace MediaBrowser.Controller.MediaEncoding { var hwType = encodingOptions.HardwareAccelerationType; - var codecMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) + var codecMap = new Dictionary<HardwareAccelerationType, string>() { - { "amf", hwEncoder + "_amf" }, - { "nvenc", hwEncoder + "_nvenc" }, - { "qsv", hwEncoder + "_qsv" }, - { "vaapi", hwEncoder + "_vaapi" }, - { "videotoolbox", hwEncoder + "_videotoolbox" }, - { "v4l2m2m", hwEncoder + "_v4l2m2m" }, - { "rkmpp", hwEncoder + "_rkmpp" }, + { HardwareAccelerationType.amf, hwEncoder + "_amf" }, + { HardwareAccelerationType.nvenc, hwEncoder + "_nvenc" }, + { HardwareAccelerationType.qsv, hwEncoder + "_qsv" }, + { HardwareAccelerationType.vaapi, hwEncoder + "_vaapi" }, + { HardwareAccelerationType.videotoolbox, hwEncoder + "_videotoolbox" }, + { HardwareAccelerationType.v4l2m2m, hwEncoder + "_v4l2m2m" }, + { HardwareAccelerationType.rkmpp, hwEncoder + "_rkmpp" }, }; - if (!string.IsNullOrEmpty(hwType) + if (hwType != HardwareAccelerationType.none && encodingOptions.EnableHardwareEncoding && codecMap.TryGetValue(hwType, out var preferredEncoder) && _mediaEncoder.SupportsEncoder(preferredEncoder)) @@ -205,7 +210,15 @@ namespace MediaBrowser.Controller.MediaEncoding { var hwType = encodingOptions.HardwareAccelerationType; - if (!string.IsNullOrEmpty(hwType) + // Only Intel has VA-API MJPEG encoder + if (hwType == HardwareAccelerationType.vaapi + && !(_mediaEncoder.IsVaapiDeviceInteliHD + || _mediaEncoder.IsVaapiDeviceInteli965)) + { + return _defaultMjpegEncoder; + } + + if (hwType != HardwareAccelerationType.none && encodingOptions.EnableHardwareEncoding && _mjpegCodecMap.TryGetValue(hwType, out var preferredEncoder) && _mediaEncoder.SupportsEncoder(preferredEncoder)) @@ -297,7 +310,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.VideoStream is null || !options.EnableTonemapping - || GetVideoColorBitDepth(state) != 10 + || GetVideoColorBitDepth(state) < 10 || !_mediaEncoder.SupportsFilter("tonemapx")) { return false; @@ -310,13 +323,12 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.VideoStream is null || !options.EnableTonemapping - || GetVideoColorBitDepth(state) != 10) + || GetVideoColorBitDepth(state) < 10) { return false; } - if (string.Equals(state.VideoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) - && state.VideoStream.VideoRange == VideoRange.HDR + if (state.VideoStream.VideoRange == VideoRange.HDR && state.VideoStream.VideoRangeType == VideoRangeType.DOVI) { // Only native SW decoder and HW accelerator can parse dovi rpu. @@ -353,7 +365,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.VideoStream is null || !options.EnableVppTonemapping - || GetVideoColorBitDepth(state) != 10) + || GetVideoColorBitDepth(state) < 10) { return false; } @@ -361,7 +373,7 @@ namespace MediaBrowser.Controller.MediaEncoding // prefer 'tonemap_vaapi' over 'vpp_qsv' on Linux for supporting Gen9/KBLx. // 'vpp_qsv' requires VPL, which is only supported on Gen12/TGLx and newer. if (OperatingSystem.IsWindows() - && string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) + && options.HardwareAccelerationType == HardwareAccelerationType.qsv && _mediaEncoder.EncoderVersion < _minFFmpegQsvVppTonemapOption) { return false; @@ -376,7 +388,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.VideoStream is null || !options.EnableVideoToolboxTonemapping - || GetVideoColorBitDepth(state) != 10) + || GetVideoColorBitDepth(state) < 10) { return false; } @@ -387,6 +399,25 @@ namespace MediaBrowser.Controller.MediaEncoding && state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.HDR10Plus or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG; } + private bool IsVideoStreamHevcRext(EncodingJobInfo state) + { + var videoStream = state.VideoStream; + if (videoStream is null) + { + return false; + } + + return string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) + && (string.Equals(videoStream.Profile, "Rext", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv422p", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv422p10le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv422p12le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase)); + } + /// <summary> /// Gets the name of the output video codec. /// </summary> @@ -851,13 +882,15 @@ namespace MediaBrowser.Controller.MediaEncoding options); } - private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string srcDeviceAlias, string alias) + private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string vendorId, string srcDeviceAlias, string alias) { alias ??= VaapiAlias; + var haveVendorId = !string.IsNullOrEmpty(vendorId) + && _mediaEncoder.EncoderVersion >= _minFFmpegVaapiDeviceVendorId; - // 'renderNodePath' has higher priority than 'kernelDriver' + // Priority: 'renderNodePath' > 'vendorId' > 'kernelDriver' var driverOpts = string.IsNullOrEmpty(renderNodePath) - ? (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver) + ? (haveVendorId ? $",vendor_id={vendorId}" : (string.IsNullOrEmpty(kernelDriver) ? string.Empty : $",kernel_driver={kernelDriver}")) : renderNodePath; // 'driver' behaves similarly to env LIBVA_DRIVER_NAME @@ -892,7 +925,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (OperatingSystem.IsLinux()) { // derive qsv from vaapi device - return GetVaapiDeviceArgs(renderNodePath, "iHD", "i915", null, VaapiAlias) + arg + "@" + VaapiAlias; + return GetVaapiDeviceArgs(renderNodePath, "iHD", "i915", "0x8086", null, VaapiAlias) + arg + "@" + VaapiAlias; } if (OperatingSystem.IsWindows()) @@ -921,7 +954,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // DVBSUB uses the fixed canvas size 720x576 if (state.SubtitleStream is not null - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + && ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream && !string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase)) { @@ -971,7 +1004,7 @@ namespace MediaBrowser.Controller.MediaEncoding var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty; var isHwTonemapAvailable = IsHwTonemapAvailable(state, options); - if (string.Equals(optHwaccelType, "vaapi", StringComparison.OrdinalIgnoreCase)) + if (optHwaccelType == HardwareAccelerationType.vaapi) { if (!isLinux || !_mediaEncoder.SupportsHwaccel("vaapi")) { @@ -987,14 +1020,14 @@ namespace MediaBrowser.Controller.MediaEncoding if (_mediaEncoder.IsVaapiDeviceInteliHD) { - args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "iHD", null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "iHD", null, null, null, VaapiAlias)); } else if (_mediaEncoder.IsVaapiDeviceInteli965) { // Only override i965 since it has lower priority than iHD in libva lookup. Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965"); Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965"); - args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "i965", null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "i965", null, null, null, VaapiAlias)); } var filterDevArgs = string.Empty; @@ -1018,7 +1051,7 @@ namespace MediaBrowser.Controller.MediaEncoding && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier) { args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias)); - args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(null, null, null, null, DrmAlias, VaapiAlias)); args.Append(GetVulkanDeviceArgs(0, null, DrmAlias, VulkanAlias)); // libplacebo wants an explicitly set vulkan filter device. @@ -1026,7 +1059,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else { - args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, null, VaapiAlias)); filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias); if (doOclTonemap) @@ -1045,7 +1078,7 @@ namespace MediaBrowser.Controller.MediaEncoding args.Append(filterDevArgs); } - else if (string.Equals(optHwaccelType, "qsv", StringComparison.OrdinalIgnoreCase)) + else if (optHwaccelType == HardwareAccelerationType.qsv) { if ((!isLinux && !isWindows) || !_mediaEncoder.SupportsHwaccel("qsv")) { @@ -1080,7 +1113,7 @@ namespace MediaBrowser.Controller.MediaEncoding args.Append(filterDevArgs); } - else if (string.Equals(optHwaccelType, "nvenc", StringComparison.OrdinalIgnoreCase)) + else if (optHwaccelType == HardwareAccelerationType.nvenc) { if ((!isLinux && !isWindows) || !IsCudaFullSupported()) { @@ -1099,7 +1132,7 @@ namespace MediaBrowser.Controller.MediaEncoding args.Append(GetCudaDeviceArgs(0, CudaAlias)) .Append(GetFilterHwDeviceArgs(CudaAlias)); } - else if (string.Equals(optHwaccelType, "amf", StringComparison.OrdinalIgnoreCase)) + else if (optHwaccelType == HardwareAccelerationType.amf) { if (!isWindows || !_mediaEncoder.SupportsHwaccel("d3d11va")) { @@ -1124,7 +1157,7 @@ namespace MediaBrowser.Controller.MediaEncoding args.Append(filterDevArgs); } - else if (string.Equals(optHwaccelType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + else if (optHwaccelType == HardwareAccelerationType.videotoolbox) { if (!isMacOS || !_mediaEncoder.SupportsHwaccel("videotoolbox")) { @@ -1141,7 +1174,7 @@ namespace MediaBrowser.Controller.MediaEncoding // videotoolbox hw filter does not require device selection args.Append(GetVideoToolboxDeviceArgs(VideotoolboxAlias)); } - else if (string.Equals(optHwaccelType, "rkmpp", StringComparison.OrdinalIgnoreCase)) + else if (optHwaccelType == HardwareAccelerationType.rkmpp) { if (!isLinux || !_mediaEncoder.SupportsHwaccel("rkmpp")) { @@ -1220,7 +1253,7 @@ namespace MediaBrowser.Controller.MediaEncoding // sub2video for external graphical subtitles if (state.SubtitleStream is not null - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + && ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleStream.IsExternal) { @@ -1414,6 +1447,148 @@ namespace MediaBrowser.Controller.MediaEncoding return FormattableString.Invariant($" -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); } + private string GetEncoderParam(EncoderPreset? preset, EncoderPreset defaultPreset, EncodingOptions encodingOptions, string videoEncoder, bool isLibX265) + { + var param = string.Empty; + var encoderPreset = preset ?? defaultPreset; + if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || isLibX265) + { + var presetString = encoderPreset switch + { + EncoderPreset.auto => EncoderPreset.veryfast.ToString().ToLowerInvariant(), + _ => encoderPreset.ToString().ToLowerInvariant() + }; + + param += " -preset " + presetString; + + int encodeCrf = encodingOptions.H264Crf; + if (isLibX265) + { + encodeCrf = encodingOptions.H265Crf; + } + + if (encodeCrf >= 0 && encodeCrf <= 51) + { + param += " -crf " + encodeCrf.ToString(CultureInfo.InvariantCulture); + } + else + { + string defaultCrf = "23"; + if (isLibX265) + { + defaultCrf = "28"; + } + + param += " -crf " + defaultCrf; + } + } + else if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)) + { + // Default to use the recommended preset 10. + // Omit presets < 5, which are too slow for on the fly encoding. + // https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Ffmpeg.md + param += encoderPreset switch + { + EncoderPreset.veryslow => " -preset 5", + EncoderPreset.slower => " -preset 6", + EncoderPreset.slow => " -preset 7", + EncoderPreset.medium => " -preset 8", + EncoderPreset.fast => " -preset 9", + EncoderPreset.faster => " -preset 10", + EncoderPreset.veryfast => " -preset 11", + EncoderPreset.superfast => " -preset 12", + EncoderPreset.ultrafast => " -preset 13", + _ => " -preset 10" + }; + } + else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase)) + { + // -compression_level is not reliable on AMD. + if (_mediaEncoder.IsVaapiDeviceInteliHD) + { + param += encoderPreset switch + { + EncoderPreset.veryslow => " -compression_level 1", + EncoderPreset.slower => " -compression_level 2", + EncoderPreset.slow => " -compression_level 3", + EncoderPreset.medium => " -compression_level 4", + EncoderPreset.fast => " -compression_level 5", + EncoderPreset.faster => " -compression_level 6", + EncoderPreset.veryfast => " -compression_level 7", + EncoderPreset.superfast => " -compression_level 7", + EncoderPreset.ultrafast => " -compression_level 7", + _ => string.Empty + }; + } + } + else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv) + || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase) // hevc (hevc_qsv) + || string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase)) // av1 (av1_qsv) + { + EncoderPreset[] valid_presets = [EncoderPreset.veryslow, EncoderPreset.slower, EncoderPreset.slow, EncoderPreset.medium, EncoderPreset.fast, EncoderPreset.faster, EncoderPreset.veryfast]; + + param += " -preset " + (valid_presets.Contains(encoderPreset) ? encoderPreset : EncoderPreset.veryfast).ToString().ToLowerInvariant(); + } + else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc) + || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) // hevc (hevc_nvenc) + || string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase) // av1 (av1_nvenc) + ) + { + param += encoderPreset switch + { + EncoderPreset.veryslow => " -preset p7", + EncoderPreset.slower => " -preset p6", + EncoderPreset.slow => " -preset p5", + EncoderPreset.medium => " -preset p4", + EncoderPreset.fast => " -preset p3", + EncoderPreset.faster => " -preset p2", + _ => " -preset p1" + }; + } + else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf) + || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) // hevc (hevc_amf) + || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase) // av1 (av1_amf) + ) + { + param += encoderPreset switch + { + EncoderPreset.veryslow => " -quality quality", + EncoderPreset.slower => " -quality quality", + EncoderPreset.slow => " -quality quality", + EncoderPreset.medium => " -quality balanced", + _ => " -quality speed" + }; + + if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) + { + param += " -header_insertion_mode gop"; + } + + if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + { + param += " -gops_per_idr 1"; + } + } + else if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase) // h264 (h264_videotoolbox) + || string.Equals(videoEncoder, "hevc_videotoolbox", StringComparison.OrdinalIgnoreCase) // hevc (hevc_videotoolbox) + ) + { + param += encoderPreset switch + { + EncoderPreset.veryslow => " -prio_speed 0", + EncoderPreset.slower => " -prio_speed 0", + EncoderPreset.slow => " -prio_speed 0", + EncoderPreset.medium => " -prio_speed 0", + _ => " -prio_speed 1" + }; + } + + return param; + } + public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level) { if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel)) @@ -1508,13 +1683,15 @@ namespace MediaBrowser.Controller.MediaEncoding setPtsParam); } - var mediaPath = state.MediaPath ?? string.Empty; + var subtitlePath = _subtitleEncoder.GetSubtitleFilePath( + state.SubtitleStream, + state.MediaSource, + CancellationToken.None).GetAwaiter().GetResult(); return string.Format( CultureInfo.InvariantCulture, - "subtitles=f='{0}':si={1}{2}{3}{4}{5}", - _mediaEncoder.EscapeSubtitleFilterPath(mediaPath), - state.InternalSubtitleStreamOffset.ToString(CultureInfo.InvariantCulture), + "subtitles=f='{0}'{1}{2}{3}{4}", + _mediaEncoder.EscapeSubtitleFilterPath(subtitlePath), alphaParam, sub2videoParam, fontParam, @@ -1534,7 +1711,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (maxrate.HasValue && state.VideoStream is not null) { - var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate; + var contentRate = state.VideoStream.ReferenceFrameRate; if (contentRate.HasValue && contentRate.Value > maxrate.Value) { @@ -1626,7 +1803,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// <param name="encodingOptions">Encoding options.</param> /// <param name="defaultPreset">Default present to use for encoding.</param> /// <returns>Video bitrate.</returns> - public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultPreset) + public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, EncoderPreset defaultPreset) { var param = string.Empty; @@ -1641,7 +1818,9 @@ namespace MediaBrowser.Controller.MediaEncoding // https://github.com/intel/media-driver/issues/1456 var enableWaFori915Hang = false; - if (string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + var hardwareAccelerationType = encodingOptions.HardwareAccelerationType; + + if (hardwareAccelerationType == HardwareAccelerationType.vaapi) { var isIntelVaapiDriver = _mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965; @@ -1654,7 +1833,7 @@ namespace MediaBrowser.Controller.MediaEncoding intelLowPowerHwEncoding = encodingOptions.EnableIntelLowPowerHevcHwEncoder && isIntelVaapiDriver; } } - else if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + else if (hardwareAccelerationType == HardwareAccelerationType.qsv) { if (OperatingSystem.IsLinux()) { @@ -1701,204 +1880,10 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -async_depth 1"; } - var isVc1 = string.Equals(state.VideoStream?.Codec, "vc1", StringComparison.OrdinalIgnoreCase); var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase); + var encodingPreset = encodingOptions.EncoderPreset; - if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || isLibX265) - { - if (!string.IsNullOrEmpty(encodingOptions.EncoderPreset)) - { - param += " -preset " + encodingOptions.EncoderPreset; - } - else - { - param += " -preset " + defaultPreset; - } - - int encodeCrf = encodingOptions.H264Crf; - if (isLibX265) - { - encodeCrf = encodingOptions.H265Crf; - } - - if (encodeCrf >= 0 && encodeCrf <= 51) - { - param += " -crf " + encodeCrf.ToString(CultureInfo.InvariantCulture); - } - else - { - string defaultCrf = "23"; - if (isLibX265) - { - defaultCrf = "28"; - } - - param += " -crf " + defaultCrf; - } - } - else if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)) - { - // Default to use the recommended preset 10. - // Omit presets < 5, which are too slow for on the fly encoding. - // https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Ffmpeg.md - param += encodingOptions.EncoderPreset switch - { - "veryslow" => " -preset 5", - "slower" => " -preset 6", - "slow" => " -preset 7", - "medium" => " -preset 8", - "fast" => " -preset 9", - "faster" => " -preset 10", - "veryfast" => " -preset 11", - "superfast" => " -preset 12", - "ultrafast" => " -preset 13", - _ => " -preset 10" - }; - } - else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase)) - { - // -compression_level is not reliable on AMD. - if (_mediaEncoder.IsVaapiDeviceInteliHD) - { - param += encodingOptions.EncoderPreset switch - { - "veryslow" => " -compression_level 1", - "slower" => " -compression_level 2", - "slow" => " -compression_level 3", - "medium" => " -compression_level 4", - "fast" => " -compression_level 5", - "faster" => " -compression_level 6", - "veryfast" => " -compression_level 7", - "superfast" => " -compression_level 7", - "ultrafast" => " -compression_level 7", - _ => string.Empty - }; - } - } - else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv) - || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase) // hevc (hevc_qsv) - || string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase)) // av1 (av1_qsv) - { - string[] valid_presets = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" }; - - if (valid_presets.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase)) - { - param += " -preset " + encodingOptions.EncoderPreset; - } - else - { - param += " -preset veryfast"; - } - } - else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc) - || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) // hevc (hevc_nvenc) - || string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase)) // av1 (av1_nvenc) - { - switch (encodingOptions.EncoderPreset) - { - case "veryslow": - param += " -preset p7"; - break; - - case "slower": - param += " -preset p6"; - break; - - case "slow": - param += " -preset p5"; - break; - - case "medium": - param += " -preset p4"; - break; - - case "fast": - param += " -preset p3"; - break; - - case "faster": - param += " -preset p2"; - break; - - case "veryfast": - case "superfast": - case "ultrafast": - param += " -preset p1"; - break; - - default: - param += " -preset p1"; - break; - } - } - else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf) - || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) // hevc (hevc_amf) - || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) // av1 (av1_amf) - { - switch (encodingOptions.EncoderPreset) - { - case "veryslow": - case "slower": - case "slow": - param += " -quality quality"; - break; - - case "medium": - param += " -quality balanced"; - break; - - case "fast": - case "faster": - case "veryfast": - case "superfast": - case "ultrafast": - param += " -quality speed"; - break; - - default: - param += " -quality speed"; - break; - } - - if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) - { - param += " -header_insertion_mode gop"; - } - - if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) - { - param += " -gops_per_idr 1"; - } - } - else if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase) // h264 (h264_videotoolbox) - || string.Equals(videoEncoder, "hevc_videotoolbox", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_videotoolbox) - { - switch (encodingOptions.EncoderPreset) - { - case "veryslow": - case "slower": - case "slow": - case "medium": - param += " -prio_speed 0"; - break; - - case "fast": - case "faster": - case "veryfast": - case "superfast": - case "ultrafast": - param += " -prio_speed 1"; - break; - - default: - param += " -prio_speed 1"; - break; - } - } - + param += GetEncoderParam(encodingPreset, defaultPreset, encodingOptions, videoEncoder, isLibX265); param += GetVideoBitrateParam(state, videoEncoder); var framerate = GetFramerateParam(state); @@ -1915,7 +1900,26 @@ namespace MediaBrowser.Controller.MediaEncoding } var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty; - profile = WhiteSpaceRegex().Replace(profile, string.Empty); + profile = WhiteSpaceRegex().Replace(profile, string.Empty).ToLowerInvariant(); + + var videoProfiles = Array.Empty<string>(); + if (string.Equals("h264", targetVideoCodec, StringComparison.OrdinalIgnoreCase)) + { + videoProfiles = _videoProfilesH264; + } + else if (string.Equals("hevc", targetVideoCodec, StringComparison.OrdinalIgnoreCase)) + { + videoProfiles = _videoProfilesH265; + } + else if (string.Equals("av1", targetVideoCodec, StringComparison.OrdinalIgnoreCase)) + { + videoProfiles = _videoProfilesAv1; + } + + if (!videoProfiles.Contains(profile, StringComparison.OrdinalIgnoreCase)) + { + profile = string.Empty; + } // We only transcode to HEVC 8-bit for now, force Main Profile. if (profile.Contains("main10", StringComparison.OrdinalIgnoreCase) @@ -2218,7 +2222,7 @@ namespace MediaBrowser.Controller.MediaEncoding var requestedFramerate = request.MaxFramerate ?? request.Framerate; if (requestedFramerate.HasValue) { - var videoFrameRate = videoStream.AverageFrameRate ?? videoStream.RealFrameRate; + var videoFrameRate = videoStream.ReferenceFrameRate; if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value) { @@ -2415,7 +2419,7 @@ namespace MediaBrowser.Controller.MediaEncoding return 1; } - private static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec) + public static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec) { var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec); var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec); @@ -2439,6 +2443,12 @@ namespace MediaBrowser.Controller.MediaEncoding { scaleFactor = Math.Max(scaleFactor, 2); } + else if (bitrate >= 30000000) + { + // Don't scale beyond 30Mbps, it is hardly visually noticeable for most codecs with our prefer speed encoding + // and will cause extremely high bitrate to be used for av1->h264 transcoding that will overload clients and encoders + scaleFactor = 1; + } return Convert.ToInt32(scaleFactor * bitrate); } @@ -2569,7 +2579,7 @@ namespace MediaBrowser.Controller.MediaEncoding } var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive; - if (state.SubtitleStream is not null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode && !isCopyingTimestamps) + if (state.SubtitleStream is not null && state.SubtitleStream.IsTextSubtitleStream && ShouldEncodeSubtitle(state) && !isCopyingTimestamps) { var seconds = TimeSpan.FromTicks(state.StartTimeTicks ?? 0).TotalSeconds; @@ -2770,7 +2780,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.AudioStream.IsExternal) { bool hasExternalGraphicsSubs = state.SubtitleStream is not null - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + && ShouldEncodeSubtitle(state) && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream; int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1; @@ -3014,6 +3024,8 @@ namespace MediaBrowser.Controller.MediaEncoding public static string GetGraphicalSubPreProcessFilters( int? videoWidth, int? videoHeight, + int? subtitleWidth, + int? subtitleHeight, int? requestedWidth, int? requestedHeight, int? requestedMaxWidth, @@ -3027,16 +3039,37 @@ namespace MediaBrowser.Controller.MediaEncoding requestedMaxWidth, requestedMaxHeight); - if (outWidth.HasValue && outHeight.HasValue) + if (!outWidth.HasValue + || !outHeight.HasValue + || outWidth.Value <= 0 + || outHeight.Value <= 0) { - return string.Format( - CultureInfo.InvariantCulture, - @"scale,scale=-1:{1}:fast_bilinear,crop,pad=max({0}\,iw):max({1}\,ih):(ow-iw)/2:(oh-ih)/2:black@0,crop={0}:{1}", - outWidth.Value, - outHeight.Value); + return string.Empty; } - return string.Empty; + // Automatically add padding based on subtitle input + var filters = @"scale,scale=-1:{1}:fast_bilinear,crop,pad=max({0}\,iw):max({1}\,ih):(ow-iw)/2:(oh-ih)/2:black@0,crop={0}:{1}"; + + if (subtitleWidth.HasValue + && subtitleHeight.HasValue + && subtitleWidth.Value > 0 + && subtitleHeight.Value > 0) + { + var videoDar = (double)outWidth.Value / outHeight.Value; + var subtitleDar = (double)subtitleWidth.Value / subtitleHeight.Value; + + // No need to add padding when DAR is the same -> 1080p PGSSUB on 2160p video + if (Math.Abs(videoDar - subtitleDar) < 0.01f) + { + filters = @"scale,scale={0}:{1}:fast_bilinear"; + } + } + + return string.Format( + CultureInfo.InvariantCulture, + filters, + outWidth.Value, + outHeight.Value); } public static string GetAlphaSrcFilter( @@ -3234,21 +3267,20 @@ namespace MediaBrowser.Controller.MediaEncoding public static string GetSwDeinterlaceFilter(EncodingJobInfo state, EncodingOptions options) { - var doubleRateDeint = options.DeinterlaceDoubleRate && state.VideoStream?.AverageFrameRate <= 30; + var doubleRateDeint = options.DeinterlaceDoubleRate && state.VideoStream?.ReferenceFrameRate <= 30; return string.Format( CultureInfo.InvariantCulture, "{0}={1}:-1:0", - string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase) ? "bwdif" : "yadif", + options.DeinterlaceMethod.ToString().ToLowerInvariant(), doubleRateDeint ? "1" : "0"); } public string GetHwDeinterlaceFilter(EncodingJobInfo state, EncodingOptions options, string hwDeintSuffix) { - var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.AverageFrameRate ?? 60) <= 30; + var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.ReferenceFrameRate ?? 60) <= 30; if (hwDeintSuffix.Contains("cuda", StringComparison.OrdinalIgnoreCase)) { - var useBwdif = string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase) - && _mediaEncoder.SupportsFilter("bwdif_cuda"); + var useBwdif = options.DeinterlaceMethod == DeinterlaceMethod.bwdif && _mediaEncoder.SupportsFilter("bwdif_cuda"); return string.Format( CultureInfo.InvariantCulture, @@ -3272,16 +3304,19 @@ namespace MediaBrowser.Controller.MediaEncoding if (hwDeintSuffix.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase)) { + var useBwdif = options.DeinterlaceMethod == DeinterlaceMethod.bwdif && _mediaEncoder.SupportsFilter("bwdif_videotoolbox"); + return string.Format( CultureInfo.InvariantCulture, - "yadif_videotoolbox={0}:-1:0", + "{0}_videotoolbox={1}:-1:0", + useBwdif ? "bwdif" : "yadif", doubleRateDeint ? "1" : "0"); } return string.Empty; } - public string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat) + private string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat, bool forceFullRange) { if (string.IsNullOrEmpty(hwTonemapSuffix)) { @@ -3289,7 +3324,10 @@ namespace MediaBrowser.Controller.MediaEncoding } var args = string.Empty; - var algorithm = options.TonemappingAlgorithm; + var algorithm = options.TonemappingAlgorithm.ToString().ToLowerInvariant(); + var mode = options.TonemappingMode.ToString().ToLowerInvariant(); + var range = forceFullRange ? TonemappingRange.pc : options.TonemappingRange; + var rangeString = range.ToString().ToLowerInvariant(); if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase)) { @@ -3324,10 +3362,10 @@ namespace MediaBrowser.Controller.MediaEncoding args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}"; var useLegacyTonemapModes = _mediaEncoder.EncoderVersion >= _minFFmpegOclCuTonemapMode - && _legacyTonemapModes.Contains(options.TonemappingMode, StringComparison.OrdinalIgnoreCase); + && _legacyTonemapModes.Contains(options.TonemappingMode); var useAdvancedTonemapModes = _mediaEncoder.EncoderVersion >= _minFFmpegAdvancedTonemapMode - && _advancedTonemapModes.Contains(options.TonemappingMode, StringComparison.OrdinalIgnoreCase); + && _advancedTonemapModes.Contains(options.TonemappingMode); if (useLegacyTonemapModes || useAdvancedTonemapModes) { @@ -3339,8 +3377,7 @@ namespace MediaBrowser.Controller.MediaEncoding args += ":param={6}"; } - if (string.Equals(options.TonemappingRange, "tv", StringComparison.OrdinalIgnoreCase) - || string.Equals(options.TonemappingRange, "pc", StringComparison.OrdinalIgnoreCase)) + if (range == TonemappingRange.tv || range == TonemappingRange.pc) { args += ":range={7}"; } @@ -3354,12 +3391,12 @@ namespace MediaBrowser.Controller.MediaEncoding algorithm, options.TonemappingPeak, options.TonemappingDesat, - options.TonemappingMode, + mode, options.TonemappingParam, - options.TonemappingRange); + rangeString); } - public string GetLibplaceboFilter( + private string GetLibplaceboFilter( EncodingOptions options, string videoFormat, bool doTonemap, @@ -3368,7 +3405,8 @@ namespace MediaBrowser.Controller.MediaEncoding int? requestedWidth, int? requestedHeight, int? requestedMaxWidth, - int? requestedMaxHeight) + int? requestedMaxHeight, + bool forceFullRange) { var (outWidth, outHeight) = GetFixedOutputSize( videoWidth, @@ -3391,24 +3429,24 @@ namespace MediaBrowser.Controller.MediaEncoding if (doTonemap) { var algorithm = options.TonemappingAlgorithm; + var algorithmString = "clip"; var mode = options.TonemappingMode; - var range = options.TonemappingRange; + var range = forceFullRange ? TonemappingRange.pc : options.TonemappingRange; - if (string.Equals(algorithm, "bt2390", StringComparison.OrdinalIgnoreCase)) + if (algorithm == TonemappingAlgorithm.bt2390) { - algorithm = "bt.2390"; + algorithmString = "bt.2390"; } - else if (string.Equals(algorithm, "none", StringComparison.OrdinalIgnoreCase)) + else if (algorithm != TonemappingAlgorithm.none) { - algorithm = "clip"; + algorithmString = algorithm.ToString().ToLowerInvariant(); } - tonemapArg = ":tonemapping=" + algorithm + ":peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709"; + tonemapArg = $":tonemapping={algorithmString}:peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709"; - if (string.Equals(range, "tv", StringComparison.OrdinalIgnoreCase) - || string.Equals(range, "pc", StringComparison.OrdinalIgnoreCase)) + if (range == TonemappingRange.tv || range == TonemappingRange.pc) { - tonemapArg += ":range=" + range; + tonemapArg += ":range=" + range.ToString().ToLowerInvariant(); } } @@ -3463,7 +3501,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doToneMap = IsSwTonemapAvailable(state, options); var requireDoviReshaping = doToneMap && state.VideoStream.VideoRangeType == VideoRangeType.DOVI; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; @@ -3512,8 +3550,8 @@ namespace MediaBrowser.Controller.MediaEncoding tonemapArgs += $":param={options.TonemappingParam}"; } - if (string.Equals(options.TonemappingRange, "tv", StringComparison.OrdinalIgnoreCase) - || string.Equals(options.TonemappingRange, "pc", StringComparison.OrdinalIgnoreCase)) + var range = options.TonemappingRange; + if (range == TonemappingRange.tv || range == TonemappingRange.pc) { tonemapArgs += $":range={options.TonemappingRange}"; } @@ -3537,7 +3575,9 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -3557,7 +3597,7 @@ namespace MediaBrowser.Controller.MediaEncoding EncodingOptions options, string vidEncoder) { - if (!string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + if (options.HardwareAccelerationType != HardwareAccelerationType.nvenc) { return (null, null, null); } @@ -3596,20 +3636,23 @@ namespace MediaBrowser.Controller.MediaEncoding var isNvencEncoder = vidEncoder.Contains("nvenc", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isNvencEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isCuInCuOut = isNvDecoder && isNvencEncoder; - var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.AverageFrameRate ?? 60) <= 30; + var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.ReferenceFrameRate ?? 60) <= 30; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); var doDeintH2645 = doDeintH264 || doDeintHevc; var doCuTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); @@ -3662,7 +3705,8 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add($"transpose_cuda=dir={tranposeDir}"); } - var outFormat = doCuTonemap ? string.Empty : "yuv420p"; + var isRext = IsVideoStreamHevcRext(state); + var outFormat = doCuTonemap ? (isRext ? "p010" : string.Empty) : "yuv420p"; var hwScaleFilter = GetHwScaleFilter("scale", "cuda", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); // hw scale mainFilters.Add(hwScaleFilter); @@ -3671,7 +3715,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw tonemap if (doCuTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "cuda", "yuv420p"); + var tonemapFilter = GetHwTonemapFilter(options, "cuda", "yuv420p", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -3713,7 +3757,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=yuva420p"); } @@ -3738,7 +3782,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -3759,7 +3803,7 @@ namespace MediaBrowser.Controller.MediaEncoding EncodingOptions options, string vidEncoder) { - if (!string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) + if (options.HardwareAccelerationType != HardwareAccelerationType.amf) { return (null, null, null); } @@ -3800,6 +3844,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isAmfEncoder = vidEncoder.Contains("amf", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isAmfEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isDxInDxOut = isD3d11vaDecoder && isAmfEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -3807,12 +3852,14 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); @@ -3877,7 +3924,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -3927,7 +3974,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=yuva420p"); } @@ -3954,7 +4001,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -3975,7 +4022,7 @@ namespace MediaBrowser.Controller.MediaEncoding EncodingOptions options, string vidEncoder) { - if (!string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + if (options.HardwareAccelerationType != HardwareAccelerationType.qsv) { return (null, null, null); } @@ -4036,6 +4083,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isHwDecoder = isD3d11vaDecoder || isQsvDecoder; var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isQsvEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isQsvInQsvOut = isHwDecoder && isQsvEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -4045,12 +4093,14 @@ namespace MediaBrowser.Controller.MediaEncoding var doOclTonemap = !doVppTonemap && IsHwTonemapAvailable(state, options); var doTonemap = doVppTonemap || doOclTonemap; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); @@ -4076,6 +4126,12 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12"); var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + if (isMjpegEncoder && !doOclTonemap) + { + // sw decoder + hw mjpeg encoder + swScaleFilter = string.IsNullOrEmpty(swScaleFilter) ? "scale=out_range=pc" : $"{swScaleFilter}:out_range=pc"; + } + // sw scale mainFilters.Add(swScaleFilter); mainFilters.Add($"format={outFormat}"); @@ -4090,6 +4146,12 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (isD3d11vaDecoder || isQsvDecoder) { + var isRext = IsVideoStreamHevcRext(state); + var twoPassVppTonemap = isRext; + var doVppFullRangeOut = isMjpegEncoder + && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption; + var doVppScaleModeHq = isMjpegEncoder + && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppScaleModeOption; var doVppProcamp = false; var procampParams = string.Empty; if (doVppTonemap) @@ -4099,34 +4161,39 @@ namespace MediaBrowser.Controller.MediaEncoding && options.VppTonemappingBrightness <= 100) { procampParams += $":brightness={options.VppTonemappingBrightness}"; - doVppProcamp = true; + twoPassVppTonemap = doVppProcamp = true; } if (options.VppTonemappingContrast > 1 && options.VppTonemappingContrast <= 10) { procampParams += $":contrast={options.VppTonemappingContrast}"; - doVppProcamp = true; + twoPassVppTonemap = doVppProcamp = true; } procampParams += doVppProcamp ? ":procamp=1:async_depth=2" : string.Empty; } - var outFormat = doOclTonemap ? (doVppTranspose ? "p010" : string.Empty) : "nv12"; - outFormat = (doVppTonemap && doVppProcamp) ? "p010" : outFormat; + var outFormat = doOclTonemap ? ((doVppTranspose || isRext) ? "p010" : string.Empty) : "nv12"; + outFormat = twoPassVppTonemap ? "p010" : outFormat; var swapOutputWandH = doVppTranspose && swapWAndH; - var hwScalePrefix = (doVppTranspose || doVppTonemap) ? "vpp" : "scale"; - var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "qsv", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var hwScaleFilter = GetHwScaleFilter("vpp", "qsv", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTranspose) { hwScaleFilter += $":transpose={tranposeDir}"; } + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) + { + hwScaleFilter += (doVppFullRangeOut && !doOclTonemap) ? ":out_range=pc" : string.Empty; + hwScaleFilter += doVppScaleModeHq ? ":scale_mode=hq" : string.Empty; + } + if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTonemap) { - hwScaleFilter += doVppProcamp ? procampParams : ":tonemap=1"; + hwScaleFilter += doVppProcamp ? procampParams : (twoPassVppTonemap ? string.Empty : ":tonemap=1"); } if (isD3d11vaDecoder) @@ -4150,7 +4217,7 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add(hwScaleFilter); // hw tonemap(w/ procamp) - if (doVppTonemap && doVppProcamp) + if (doVppTonemap && twoPassVppTonemap) { mainFilters.Add("vpp_qsv=tonemap=1:format=nv12:async_depth=2"); } @@ -4171,7 +4238,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4223,7 +4290,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (hasGraphicalSubs) { // overlay_qsv can handle overlay scaling, setup a smaller height to reduce transfer overhead - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, 1080); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, 1080); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -4259,7 +4326,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -4288,6 +4355,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isHwDecoder = isVaapiDecoder || isQsvDecoder; var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isQsvEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isQsvInQsvOut = isHwDecoder && isQsvEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -4297,12 +4365,14 @@ namespace MediaBrowser.Controller.MediaEncoding var doTonemap = doVaVppTonemap || doOclTonemap; var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); @@ -4328,6 +4398,12 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12"); var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + if (isMjpegEncoder && !doOclTonemap) + { + // sw decoder + hw mjpeg encoder + swScaleFilter = string.IsNullOrEmpty(swScaleFilter) ? "scale=out_range=pc" : $"{swScaleFilter}:out_range=pc"; + } + // sw scale mainFilters.Add(swScaleFilter); mainFilters.Add($"format={outFormat}"); @@ -4343,6 +4419,11 @@ namespace MediaBrowser.Controller.MediaEncoding else if (isVaapiDecoder || isQsvDecoder) { var hwFilterSuffix = isVaapiDecoder ? "vaapi" : "qsv"; + var isRext = IsVideoStreamHevcRext(state); + var doVppFullRangeOut = isMjpegEncoder + && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption; + var doVppScaleModeHq = isMjpegEncoder + && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppScaleModeOption; // INPUT vaapi/qsv surface(vram) // hw deint @@ -4358,9 +4439,9 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add($"transpose_vaapi=dir={tranposeDir}"); } - var outFormat = doOclTonemap ? ((isQsvDecoder && doVppTranspose) ? "p010" : string.Empty) : "nv12"; + var outFormat = doTonemap ? (((isQsvDecoder && doVppTranspose) || isRext) ? "p010" : string.Empty) : "nv12"; var swapOutputWandH = isQsvDecoder && doVppTranspose && swapWAndH; - var hwScalePrefix = (isQsvDecoder && doVppTranspose) ? "vpp" : "scale"; + var hwScalePrefix = isQsvDecoder ? "vpp" : "scale"; var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, hwFilterSuffix, outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); if (!string.IsNullOrEmpty(hwScaleFilter) && isQsvDecoder && doVppTranspose) @@ -4368,6 +4449,12 @@ namespace MediaBrowser.Controller.MediaEncoding hwScaleFilter += $":transpose={tranposeDir}"; } + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) + { + hwScaleFilter += ((isQsvDecoder && !doVppFullRangeOut) || doOclTonemap) ? string.Empty : ":out_range=pc"; + hwScaleFilter += isQsvDecoder ? (doVppScaleModeHq ? ":scale_mode=hq" : string.Empty) : ":mode=hq"; + } + // allocate extra pool sizes for vaapi vpp scale if (!string.IsNullOrEmpty(hwScaleFilter) && isVaapiDecoder) { @@ -4388,7 +4475,7 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add("format=vaapi"); } - var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); if (isQsvDecoder) @@ -4408,7 +4495,7 @@ namespace MediaBrowser.Controller.MediaEncoding // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4469,7 +4556,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (hasGraphicalSubs) { // overlay_qsv can handle overlay scaling, setup a smaller height to reduce transfer overhead - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, 1080); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, 1080); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -4504,7 +4591,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -4525,7 +4612,7 @@ namespace MediaBrowser.Controller.MediaEncoding EncodingOptions options, string vidEncoder) { - if (!string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + if (options.HardwareAccelerationType != HardwareAccelerationType.vaapi) { return (null, null, null); } @@ -4599,6 +4686,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isVaapiEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isVaInVaOut = isVaapiDecoder && isVaapiEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -4608,12 +4696,14 @@ namespace MediaBrowser.Controller.MediaEncoding var doTonemap = doVaVppTonemap || doOclTonemap; var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); @@ -4639,6 +4729,12 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doOclTonemap ? "yuv420p10le" : "nv12"; var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + if (isMjpegEncoder && !doOclTonemap) + { + // sw decoder + hw mjpeg encoder + swScaleFilter = string.IsNullOrEmpty(swScaleFilter) ? "scale=out_range=pc" : $"{swScaleFilter}:out_range=pc"; + } + // sw scale mainFilters.Add(swScaleFilter); mainFilters.Add($"format={outFormat}"); @@ -4653,6 +4749,8 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (isVaapiDecoder) { + var isRext = IsVideoStreamHevcRext(state); + // INPUT vaapi surface(vram) // hw deint if (doDeintH2645) @@ -4667,9 +4765,15 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add($"transpose_vaapi=dir={tranposeDir}"); } - var outFormat = doTonemap ? string.Empty : "nv12"; + var outFormat = doTonemap ? (isRext ? "p010" : string.Empty) : "nv12"; var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) + { + hwScaleFilter += doOclTonemap ? string.Empty : ":out_range=pc"; + hwScaleFilter += ":mode=hq"; + } + // allocate extra pool sizes for vaapi vpp if (!string.IsNullOrEmpty(hwScaleFilter)) { @@ -4683,7 +4787,7 @@ namespace MediaBrowser.Controller.MediaEncoding // vaapi vpp tonemap if (doVaVppTonemap && isVaapiDecoder) { - var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4696,7 +4800,7 @@ namespace MediaBrowser.Controller.MediaEncoding // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4755,7 +4859,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (hasGraphicalSubs) { // overlay_vaapi can handle overlay scaling, setup a smaller height to reduce transfer overhead - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, 1080); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, 1080); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -4788,7 +4892,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); @@ -4820,13 +4924,14 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isVaapiEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); var doVkTonemap = IsVulkanHwTonemapAvailable(state, options); var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -4915,6 +5020,12 @@ namespace MediaBrowser.Controller.MediaEncoding // hw scale var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", "nv12", false, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder && !doVkTonemap) + { + hwScaleFilter += ":out_range=pc:mode=hq"; + } + mainFilters.Add(hwScaleFilter); } } @@ -4935,7 +5046,7 @@ namespace MediaBrowser.Controller.MediaEncoding // vk libplacebo if (doVkTonemap || hasSubs) { - var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, isMjpegEncoder); mainFilters.Add(libplaceboFilter); mainFilters.Add("format=vulkan"); } @@ -4980,7 +5091,9 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -5048,6 +5161,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isVaapiEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isVaInVaOut = isVaapiDecoder && isVaapiEncoder; var isi965Driver = _mediaEncoder.IsVaapiDeviceInteli965; var isAmdDriver = _mediaEncoder.IsVaapiDeviceAmd; @@ -5057,7 +5171,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; @@ -5084,6 +5198,12 @@ namespace MediaBrowser.Controller.MediaEncoding outFormat = doOclTonemap ? "yuv420p10le" : "nv12"; var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + if (isMjpegEncoder && !doOclTonemap) + { + // sw decoder + hw mjpeg encoder + swScaleFilter = string.IsNullOrEmpty(swScaleFilter) ? "scale=out_range=pc" : $"{swScaleFilter}:out_range=pc"; + } + // sw scale mainFilters.Add(swScaleFilter); mainFilters.Add("format=" + outFormat); @@ -5109,6 +5229,12 @@ namespace MediaBrowser.Controller.MediaEncoding outFormat = doOclTonemap ? string.Empty : "nv12"; var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) + { + hwScaleFilter += doOclTonemap ? string.Empty : ":out_range=pc"; + hwScaleFilter += ":mode=hq"; + } + // allocate extra pool sizes for vaapi vpp if (!string.IsNullOrEmpty(hwScaleFilter)) { @@ -5137,7 +5263,7 @@ namespace MediaBrowser.Controller.MediaEncoding // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -5203,7 +5329,9 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); @@ -5229,7 +5357,7 @@ namespace MediaBrowser.Controller.MediaEncoding EncodingOptions options, string vidEncoder) { - if (!string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + if (options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox) { return (null, null, null); } @@ -5261,6 +5389,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase); var isVtDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase); + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var inW = state.VideoStream?.Width; var inH = state.VideoStream?.Height; @@ -5303,7 +5432,7 @@ namespace MediaBrowser.Controller.MediaEncoding var hwScaleFilter = GetHwScaleFilter("scale", "vt", scaleFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -5342,7 +5471,7 @@ namespace MediaBrowser.Controller.MediaEncoding // Metal tonemap if (doMetalTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "videotoolbox", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "videotoolbox", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -5354,7 +5483,9 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -5418,7 +5549,7 @@ namespace MediaBrowser.Controller.MediaEncoding EncodingOptions options, string vidEncoder) { - if (!string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase)) + if (options.HardwareAccelerationType != HardwareAccelerationType.rkmpp) { return (null, null, null); } @@ -5463,19 +5594,25 @@ namespace MediaBrowser.Controller.MediaEncoding var isRkmppEncoder = vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase); var isSwDecoder = !isRkmppDecoder; var isSwEncoder = !isRkmppEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isDrmInDrmOut = isRkmppDecoder && isRkmppEncoder; + var isEncoderSupportAfbc = isRkmppEncoder + && (vidEncoder.Contains("h264", StringComparison.OrdinalIgnoreCase) + || vidEncoder.Contains("hevc", StringComparison.OrdinalIgnoreCase)); var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream != null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); @@ -5501,6 +5638,12 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12"); var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + if (isMjpegEncoder && !doOclTonemap) + { + // sw decoder + hw mjpeg encoder + swScaleFilter = string.IsNullOrEmpty(swScaleFilter) ? "scale=out_range=pc" : $"{swScaleFilter}:out_range=pc"; + } + if (!string.IsNullOrEmpty(swScaleFilter)) { swScaleFilter += ":flags=fast_bilinear"; @@ -5522,18 +5665,26 @@ namespace MediaBrowser.Controller.MediaEncoding { // INPUT rkmpp/drm surface(gem/dma-heap) - var isFullAfbcPipeline = isDrmInDrmOut && !doOclTonemap; + var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap; var swapOutputWandH = doRkVppTranspose && swapWAndH; - var outFormat = doOclTonemap ? "p010" : "nv12"; - var hwScalePrefix = doRkVppTranspose ? "vpp" : "scale"; - var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); - var hwScaleFilter2 = GetHwScaleFilter(hwScalePrefix, "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var outFormat = doOclTonemap ? "p010" : (isMjpegEncoder ? "bgra" : "nv12"); // RGA only support full range in rgb fmts + var hwScaleFilter = GetHwScaleFilter("vpp", "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var doScaling = GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); if (!hasSubs || doRkVppTranspose || !isFullAfbcPipeline - || !string.IsNullOrEmpty(hwScaleFilter2)) + || !string.IsNullOrEmpty(doScaling)) { + // RGA3 hardware only support (1/8 ~ 8) scaling in each blit operation, + // but in Trickplay there's a case: (3840/320 == 12), enable 2pass for it + if (!string.IsNullOrEmpty(doScaling) + && !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f)) + { + var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={outFormat}:afbc=1"; + mainFilters.Add(hwScaleFilterFirstPass); + } + if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose) { hwScaleFilter += $":transpose={tranposeDir}"; @@ -5553,19 +5704,13 @@ namespace MediaBrowser.Controller.MediaEncoding if (doOclTonemap && isRkmppDecoder) { // map from rkmpp/drm to opencl via drm-opencl interop. - mainFilters.Add("hwmap=derive_device=opencl:mode=read"); + mainFilters.Add("hwmap=derive_device=opencl"); } // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); - // enable tradeoffs for performance - if (!string.IsNullOrEmpty(tonemapFilter)) - { - tonemapFilter += ":tradeoff=1"; - } - + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -5602,7 +5747,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // OUTPUT drm(nv12) surface(gem/dma-heap) // reverse-mapping via drm-opencl interop. - mainFilters.Add("hwmap=derive_device=rkmpp:mode=write:reverse=1"); + mainFilters.Add("hwmap=derive_device=rkmpp:reverse=1"); mainFilters.Add("format=drm_prime"); } } @@ -5616,7 +5761,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -5636,14 +5781,20 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters.Add("hwupload=derive_device=rkmpp"); // try enabling AFBC to save DDR bandwidth - overlayFilters.Add("overlay_rkrga=eof_action=pass:repeatlast=0:format=nv12:afbc=1"); + var hwOverlayFilter = "overlay_rkrga=eof_action=pass:repeatlast=0:format=nv12"; + if (isEncoderSupportAfbc) + { + hwOverlayFilter += ":afbc=1"; + } + + overlayFilters.Add(hwOverlayFilter); } } else if (memoryOutput) { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -5670,7 +5821,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Empty; } - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; @@ -5678,38 +5829,20 @@ namespace MediaBrowser.Controller.MediaEncoding List<string> subFilters; List<string> overlayFilters; - if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) - { - (mainFilters, subFilters, overlayFilters) = GetVaapiVidFilterChain(state, options, outputVideoCodec); - } - else if (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) - { - (mainFilters, subFilters, overlayFilters) = GetIntelVidFilterChain(state, options, outputVideoCodec); - } - else if (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) - { - (mainFilters, subFilters, overlayFilters) = GetNvidiaVidFilterChain(state, options, outputVideoCodec); - } - else if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) - { - (mainFilters, subFilters, overlayFilters) = GetAmdVidFilterChain(state, options, outputVideoCodec); - } - else if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + (mainFilters, subFilters, overlayFilters) = options.HardwareAccelerationType switch { - (mainFilters, subFilters, overlayFilters) = GetAppleVidFilterChain(state, options, outputVideoCodec); - } - else if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase)) - { - (mainFilters, subFilters, overlayFilters) = GetRkmppVidFilterChain(state, options, outputVideoCodec); - } - else - { - (mainFilters, subFilters, overlayFilters) = GetSwVidFilterChain(state, options, outputVideoCodec); - } + HardwareAccelerationType.vaapi => GetVaapiVidFilterChain(state, options, outputVideoCodec), + HardwareAccelerationType.amf => GetAmdVidFilterChain(state, options, outputVideoCodec), + HardwareAccelerationType.qsv => GetIntelVidFilterChain(state, options, outputVideoCodec), + HardwareAccelerationType.nvenc => GetNvidiaVidFilterChain(state, options, outputVideoCodec), + HardwareAccelerationType.videotoolbox => GetAppleVidFilterChain(state, options, outputVideoCodec), + HardwareAccelerationType.rkmpp => GetRkmppVidFilterChain(state, options, outputVideoCodec), + _ => GetSwVidFilterChain(state, options, outputVideoCodec), + }; - mainFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter)); - subFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter)); - overlayFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter)); + mainFilters?.RemoveAll(string.IsNullOrEmpty); + subFilters?.RemoveAll(string.IsNullOrEmpty); + overlayFilters?.RemoveAll(string.IsNullOrEmpty); var framerate = GetFramerateParam(state); if (framerate.HasValue) @@ -5889,7 +6022,9 @@ namespace MediaBrowser.Controller.MediaEncoding return null; } - if (!string.IsNullOrEmpty(videoStream.Codec) && !string.IsNullOrEmpty(options.HardwareAccelerationType)) + var hardwareAccelerationType = options.HardwareAccelerationType; + + if (!string.IsNullOrEmpty(videoStream.Codec) && hardwareAccelerationType != HardwareAccelerationType.none) { var bitDepth = GetVideoColorBitDepth(state); @@ -5901,10 +6036,10 @@ namespace MediaBrowser.Controller.MediaEncoding || string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase))) { // RKMPP has H.264 Hi10P decoder - bool hasHardwareHi10P = string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase); + bool hasHardwareHi10P = hardwareAccelerationType == HardwareAccelerationType.rkmpp; // VideoToolbox on Apple Silicon has H.264 Hi10P mode enabled after macOS 14.6 - if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + if (hardwareAccelerationType == HardwareAccelerationType.videotoolbox) { var ver = Environment.OSVersion.Version; var arch = RuntimeInformation.OSArchitecture; @@ -5921,50 +6056,23 @@ namespace MediaBrowser.Controller.MediaEncoding } } - if (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) - { - return GetQsvHwVidDecoder(state, options, videoStream, bitDepth); - } - - if (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) - { - return GetNvdecVidDecoder(state, options, videoStream, bitDepth); - } - - if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) - { - return GetAmfVidDecoder(state, options, videoStream, bitDepth); - } - - if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) - { - return GetVaapiVidDecoder(state, options, videoStream, bitDepth); - } - - if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + var decoder = hardwareAccelerationType switch { - return GetVideotoolboxVidDecoder(state, options, videoStream, bitDepth); - } + HardwareAccelerationType.vaapi => GetVaapiVidDecoder(state, options, videoStream, bitDepth), + HardwareAccelerationType.amf => GetAmfVidDecoder(state, options, videoStream, bitDepth), + HardwareAccelerationType.qsv => GetQsvHwVidDecoder(state, options, videoStream, bitDepth), + HardwareAccelerationType.nvenc => GetNvdecVidDecoder(state, options, videoStream, bitDepth), + HardwareAccelerationType.videotoolbox => GetVideotoolboxVidDecoder(state, options, videoStream, bitDepth), + HardwareAccelerationType.rkmpp => GetRkmppVidDecoder(state, options, videoStream, bitDepth), + _ => string.Empty + }; - if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(decoder)) { - return GetRkmppVidDecoder(state, options, videoStream, bitDepth); + return decoder; } } - var whichCodec = videoStream.Codec; - if (string.Equals(whichCodec, "avc", StringComparison.OrdinalIgnoreCase)) - { - whichCodec = "h264"; - } - else if (string.Equals(whichCodec, "h265", StringComparison.OrdinalIgnoreCase)) - { - whichCodec = "hevc"; - } - - // Avoid a second attempt if no hardware acceleration is being used - options.HardwareDecodingCodecs = Array.FindAll(options.HardwareDecodingCodecs, val => !string.Equals(val, whichCodec, StringComparison.OrdinalIgnoreCase)); - // leave blank so ffmpeg will decide return null; } @@ -5988,7 +6096,11 @@ namespace MediaBrowser.Controller.MediaEncoding var decoderName = decoderPrefix + '_' + decoderSuffix; var isCodecAvailable = _mediaEncoder.SupportsDecoder(decoderName) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase); - if (bitDepth == 10 && isCodecAvailable) + + // VideoToolbox decoders have built-in SW fallback + if (bitDepth == 10 + && isCodecAvailable + && (options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox)) { if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase) && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase) @@ -6044,6 +6156,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isVideotoolboxSupported = isMacOS && _mediaEncoder.SupportsHwaccel("videotoolbox"); var isRkmppSupported = isLinux && IsRkmppFullSupported(); var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase); + var hardwareAccelerationType = options.HardwareAccelerationType; var ffmpegVersion = _mediaEncoder.EncoderVersion; @@ -6063,17 +6176,40 @@ namespace MediaBrowser.Controller.MediaEncoding && ffmpegVersion >= _minFFmpegDisplayRotationOption; var stripRotationDataArgs = stripRotationData ? " -display_rotation 0" : string.Empty; - if (bitDepth == 10 && isCodecAvailable) + // VideoToolbox decoders have built-in SW fallback + if (isCodecAvailable + && (options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox)) { if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase) - && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase) - && !options.EnableDecodingColorDepth10Hevc) + && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase)) { - return null; + if (IsVideoStreamHevcRext(state)) + { + if (bitDepth <= 10 && !options.EnableDecodingColorDepth10HevcRext) + { + return null; + } + + if (bitDepth == 12 && !options.EnableDecodingColorDepth12HevcRext) + { + return null; + } + + if (hardwareAccelerationType == HardwareAccelerationType.vaapi + && !_mediaEncoder.IsVaapiDeviceInteliHD) + { + return null; + } + } + else if (bitDepth == 10 && !options.EnableDecodingColorDepth10Hevc) + { + return null; + } } if (string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase) && options.HardwareDecodingCodecs.Contains("vp9", StringComparison.OrdinalIgnoreCase) + && bitDepth == 10 && !options.EnableDecodingColorDepth10Vp9) { return null; @@ -6081,7 +6217,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Intel qsv/d3d11va/vaapi - if (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + if (hardwareAccelerationType == HardwareAccelerationType.qsv) { if (options.PreferSystemNativeHwDecoder) { @@ -6107,7 +6243,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Nvidia cuda - if (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + if (hardwareAccelerationType == HardwareAccelerationType.nvenc) { if (isCudaSupported && isCodecAvailable) { @@ -6124,7 +6260,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Amd d3d11va - if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) + if (hardwareAccelerationType == HardwareAccelerationType.amf) { if (isD3d11Supported && isCodecAvailable) { @@ -6134,7 +6270,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Vaapi - if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) + if (hardwareAccelerationType == HardwareAccelerationType.vaapi && isVaapiSupported && isCodecAvailable) { @@ -6143,7 +6279,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Apple videotoolbox - if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase) + if (hardwareAccelerationType == HardwareAccelerationType.videotoolbox && isVideotoolboxSupported && isCodecAvailable) { @@ -6151,7 +6287,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Rockchip rkmpp - if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase) + if (hardwareAccelerationType == HardwareAccelerationType.rkmpp && isRkmppSupported && isCodecAvailable) { @@ -6167,7 +6303,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isLinux = OperatingSystem.IsLinux(); if ((!isWindows && !isLinux) - || !string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + || options.HardwareAccelerationType != HardwareAccelerationType.qsv) { return null; } @@ -6185,6 +6321,14 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsQsv = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsQsv = is8bitSwFormatsQsv || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsQsv = is8_10bitSwFormatsQsv + || string.Equals("yuv422p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); // TODO: add more 8/10bit and 4:4:4 formats for Qsv after finishing the ffcheck tool if (is8bitSwFormatsQsv) @@ -6213,12 +6357,6 @@ namespace MediaBrowser.Controller.MediaEncoding if (is8_10bitSwFormatsQsv) { - if (string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)) - { - return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "qsv", "hevc", bitDepth); - } - if (string.Equals(videoStream.Codec, "vp9", StringComparison.OrdinalIgnoreCase)) { return GetHwaccelType(state, options, "vp9", bitDepth, hwSurface) + GetHwDecoderName(options, "vp9", "qsv", "vp9", bitDepth); @@ -6230,13 +6368,22 @@ namespace MediaBrowser.Controller.MediaEncoding } } + if (is8_10_12bitSwFormatsQsv) + { + if (string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)) + { + return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "qsv", "hevc", bitDepth); + } + } + return null; } public string GetNvdecVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth) { if ((!OperatingSystem.IsWindows() && !OperatingSystem.IsLinux()) - || !string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + || options.HardwareAccelerationType != HardwareAccelerationType.nvenc) { return null; } @@ -6245,6 +6392,11 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsNvdec = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsNvdec = is8bitSwFormatsNvdec || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsNvdec = is8_10bitSwFormatsNvdec + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); // TODO: add more 8/10/12bit and 4:4:4 formats for Nvdec after finishing the ffcheck tool if (is8bitSwFormatsNvdec) @@ -6278,12 +6430,6 @@ namespace MediaBrowser.Controller.MediaEncoding if (is8_10bitSwFormatsNvdec) { - if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) - || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) - { - return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "cuvid", "hevc", bitDepth); - } - if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { return GetHwaccelType(state, options, "vp9", bitDepth, hwSurface) + GetHwDecoderName(options, "vp9", "cuvid", "vp9", bitDepth); @@ -6295,13 +6441,22 @@ namespace MediaBrowser.Controller.MediaEncoding } } + if (is8_10_12bitSwFormatsNvdec) + { + if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) + || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + { + return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "cuvid", "hevc", bitDepth); + } + } + return null; } public string GetAmfVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth) { if (!OperatingSystem.IsWindows() - || !string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) + || options.HardwareAccelerationType != HardwareAccelerationType.amf) { return null; } @@ -6357,7 +6512,7 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetVaapiVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth) { if (!OperatingSystem.IsLinux() - || !string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + || options.HardwareAccelerationType != HardwareAccelerationType.vaapi) { return null; } @@ -6369,6 +6524,14 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsVaapi = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsVaapi = is8bitSwFormatsVaapi || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsVaapi = is8_10bitSwFormatsVaapi + || string.Equals("yuv422p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); if (is8bitSwFormatsVaapi) { @@ -6396,12 +6559,6 @@ namespace MediaBrowser.Controller.MediaEncoding if (is8_10bitSwFormatsVaapi) { - if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) - || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) - { - return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface); - } - if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { return GetHwaccelType(state, options, "vp9", bitDepth, hwSurface); @@ -6413,13 +6570,22 @@ namespace MediaBrowser.Controller.MediaEncoding } } + if (is8_10_12bitSwFormatsVaapi) + { + if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) + || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + { + return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface); + } + } + return null; } public string GetVideotoolboxVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth) { if (!OperatingSystem.IsMacOS() - || !string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + || options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox) { return null; } @@ -6427,6 +6593,14 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsVt = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsVt = is8_10bitSwFormatsVt + || string.Equals("yuv422p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); // The related patches make videotoolbox hardware surface working is only available in jellyfin-ffmpeg 7.0.1 at the moment. bool useHwSurface = (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface) && IsVideoToolboxFullSupported(); @@ -6447,15 +6621,18 @@ namespace MediaBrowser.Controller.MediaEncoding return GetHwaccelType(state, options, "h264", bitDepth, useHwSurface); } - if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) - || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { - return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface); + return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface); } + } - if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + if (is8_10_12bitSwFormatsVt) + { + if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) + || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { - return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface); + return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface); } } @@ -6467,7 +6644,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isLinux = OperatingSystem.IsLinux(); if (!isLinux - || !string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase)) + || options.HardwareAccelerationType != HardwareAccelerationType.rkmpp) { return null; } @@ -6680,7 +6857,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(state.InputVideoSync)) { - inputModifier += " -vsync " + state.InputVideoSync; + inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion); } if (state.ReadInputAtNativeFramerate && state.InputProtocol != MediaProtocol.Rtsp) @@ -6731,7 +6908,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.IsVideoRequest) { - if (!string.IsNullOrEmpty(state.InputContainer) && state.VideoType == VideoType.VideoFile && string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType)) + if (!string.IsNullOrEmpty(state.InputContainer) && state.VideoType == VideoType.VideoFile && encodingOptions.HardwareAccelerationType != HardwareAccelerationType.none) { var inputFormat = GetInputFormat(state.InputContainer); if (!string.IsNullOrEmpty(inputFormat)) @@ -6847,7 +7024,7 @@ namespace MediaBrowser.Controller.MediaEncoding state.SupportedAudioCodecs = supportedAudioCodecsList.ToArray(); - request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToAudioCodec(i)) + request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(_mediaEncoder.CanEncodeToAudioCodec) ?? state.SupportedAudioCodecs.FirstOrDefault(); } @@ -6973,7 +7150,7 @@ namespace MediaBrowser.Controller.MediaEncoding return " -codec:s:0 " + codec + " -disposition:s:0 default"; } - public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string defaultPreset) + public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, EncoderPreset defaultPreset) { // Get the output codec name var videoCodec = GetVideoEncoder(state, encodingOptions); @@ -7024,7 +7201,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Empty; } - public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, string defaultPreset) + public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, EncoderPreset defaultPreset) { var args = "-codec:v:0 " + videoCodec; @@ -7065,7 +7242,7 @@ namespace MediaBrowser.Controller.MediaEncoding args += keyFrameArg; - var hasGraphicalSubs = state.SubtitleStream is not null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasGraphicalSubs = state.SubtitleStream is not null && !state.SubtitleStream.IsTextSubtitleStream && ShouldEncodeSubtitle(state); var hasCopyTs = false; @@ -7103,7 +7280,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(state.OutputVideoSync)) { - args += " -vsync " + state.OutputVideoSync; + args += GetVideoSyncOption(state.OutputVideoSync, _mediaEncoder.EncoderVersion); } args += GetOutputFFlags(state); @@ -7270,5 +7447,39 @@ namespace MediaBrowser.Controller.MediaEncoding { return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase); } + + private static bool ShouldEncodeSubtitle(EncodingJobInfo state) + { + return state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + || (state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding && !IsCopyCodec(state.OutputVideoCodec)); + } + + public static string GetVideoSyncOption(string videoSync, Version encoderVersion) + { + if (string.IsNullOrEmpty(videoSync)) + { + return string.Empty; + } + + if (encoderVersion >= new Version(5, 1)) + { + if (int.TryParse(videoSync, CultureInfo.InvariantCulture, out var vsync)) + { + return vsync switch + { + -1 => " -fps_mode auto", + 0 => " -fps_mode passthrough", + 1 => " -fps_mode cfr", + 2 => " -fps_mode vfr", + _ => string.Empty + }; + } + + return string.Empty; + } + + // -vsync is deprecated in FFmpeg 5.1 and will be removed in the future. + return $" -vsync {videoSync}"; + } } } |
