diff options
Diffstat (limited to 'MediaBrowser.Controller/MediaEncoding')
3 files changed, 125 insertions, 17 deletions
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index a0f4087395..ebfa78176a 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -88,6 +88,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1); private readonly Version _minFFmpegReadrateCatchupOption = new Version(8, 0); private readonly Version _minFFmpegNoiseBsfDrop = new Version(5, 0); + private readonly Version _minFFmpegAmfAv1EncCAQ = new Version(7, 1); private static readonly string[] _videoProfilesH264 = [ @@ -1668,14 +1669,6 @@ namespace MediaBrowser.Controller.MediaEncoding // TODO: probe QSV encoders' capabilities and enable more tuning options // See also https://github.com/intel/media-delivery/blob/master/doc/quality.rst - // Enable MacroBlock level bitrate control for better subjective visual quality - var mbbrcOpt = string.Empty; - if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) - { - mbbrcOpt = " -mbbrc 1"; - } - // Some less powerful H.264 HW decoders require strict CPB size // So bufsize optimizations should not be applied to them int factor = 2; @@ -1696,7 +1689,7 @@ namespace MediaBrowser.Controller.MediaEncoding int qsvInitOcc = (int)Math.Min((long)bitrate * 1 * factor, int.MaxValue); int qsvBufsize = (int)Math.Min((long)bitrate * 2 * factor, int.MaxValue); - return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {qsvMaxrate} -rc_init_occupancy {qsvInitOcc} -bufsize {qsvBufsize}"); + return string.Create(CultureInfo.InvariantCulture, $" -b:v {bitrate} -maxrate {qsvMaxrate} -rc_init_occupancy {qsvInitOcc} -bufsize {qsvBufsize}"); } if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase) @@ -1813,6 +1806,13 @@ namespace MediaBrowser.Controller.MediaEncoding 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(); + + // Enable MacroBlock level bitrate control for better subjective visual quality + if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) + { + param += " -mbbrc 1"; + } } else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc) || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) // hevc (hevc_nvenc) @@ -1829,6 +1829,13 @@ namespace MediaBrowser.Controller.MediaEncoding EncoderPreset.faster => " -preset p2", _ => " -preset p1" }; + + // Enable Spatial Adaptive Quantization for better subjective visual quality + // H.264 is skipped for now because Maxwell 1 does not support it + if (!string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) + { + param += " -spatial-aq 1 -aq-strength 15"; + } } else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf) || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) // hevc (hevc_amf) @@ -1844,6 +1851,18 @@ namespace MediaBrowser.Controller.MediaEncoding _ => " -quality speed" }; + // Enable Variance Based/Context Adaptive Quantization for better subjective visual quality + if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + { + param += " -vbaq 1"; + } + else if (string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase) + && _mediaEncoder.EncoderVersion >= _minFFmpegAmfAv1EncCAQ) + { + param += " -aq_mode caq"; + } + if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) { @@ -2693,6 +2712,28 @@ namespace MediaBrowser.Controller.MediaEncoding return reasons; } + private static int GetBitDepthCompensatedBitrate(int bitrate, MediaStream videoStream, int targetBitDepth) + { + if (targetBitDepth >= 10 + || (videoStream.BitDepth is int bitDepth && bitDepth <= targetBitDepth) + || videoStream.BitRate is not int sourceBitrate) + { + return bitrate; + } + + // Bit depth reduction requires bit rate compensation because + // 10-bit encoded video is significantly more efficient. + var factor = sourceBitrate switch + { + <= 4_000_000 => 1.5, + <= 8_000_000 => 1.2, + <= 12_000_000 => 1.1, + _ => 1.05 + }; + + return Convert.ToInt32(bitrate * factor); + } + public int GetVideoBitrateParamValue(BaseEncodingJobOptions request, MediaStream videoStream, string outputVideoCodec) { var bitrate = request.VideoBitRate; @@ -2715,6 +2756,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (bitrate.HasValue) { var inputVideoCodec = videoStream.Codec; + bitrate = GetBitDepthCompensatedBitrate(bitrate.Value, videoStream, 8); bitrate = ScaleBitrate(bitrate.Value, inputVideoCodec, outputVideoCodec); // If a max bitrate was requested, don't let the scaled bitrate exceed it @@ -3807,6 +3849,11 @@ namespace MediaBrowser.Controller.MediaEncoding var formatArg = isFormatFixed ? (":format=" + videoFormat) : string.Empty; var tonemapArg = string.Empty; + // libplacebo only support full range RGB + forceFullRange = forceFullRange + || (videoFormat ?? string.Empty).Contains("rgb", StringComparison.OrdinalIgnoreCase) + || (videoFormat ?? string.Empty).Contains("bgr", StringComparison.OrdinalIgnoreCase); + if (doTonemap) { var algorithm = options.TonemappingAlgorithm; @@ -3830,6 +3877,10 @@ namespace MediaBrowser.Controller.MediaEncoding tonemapArg += ":range=" + range.ToString().ToLowerInvariant(); } } + else if (forceFullRange) + { + formatArg += ":range=pc"; + } return string.Format( CultureInfo.InvariantCulture, @@ -5470,7 +5521,14 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add("format=vaapi"); // clear the surf->meta_offset and output nv12 - mainFilters.Add("scale_vaapi=format=nv12"); + var hwCscFilter = "scale_vaapi=format=nv12"; + + if (!isMjpegEncoder && options.TonemappingRange != TonemappingRange.pc) + { + hwCscFilter += ":out_range=tv"; + } + + mainFilters.Add(hwCscFilter); // hw deint if (doDeintH2645) @@ -5540,7 +5598,14 @@ namespace MediaBrowser.Controller.MediaEncoding overlayFilters.Add("format=vaapi"); // clear the surf->meta_offset and output nv12 - overlayFilters.Add("scale_vaapi=format=nv12"); + var hwCscFilter = "scale_vaapi=format=nv12"; + + if (!doVkTonemap || (doVkTonemap && options.TonemappingRange != TonemappingRange.pc)) + { + hwCscFilter += ":out_range=tv"; + } + + overlayFilters.Add(hwCscFilter); // hw deint if (doDeintH2645) @@ -7809,7 +7874,9 @@ namespace MediaBrowser.Controller.MediaEncoding var channels = state.OutputAudioChannels; - var useDownMixAlgorithm = state.AudioStream is not null + // Must match the condition under which GetAudioFilterParam emits the downmix filter. + var useDownMixAlgorithm = channels == 2 + && state.AudioStream?.Channels > 2 && DownMixAlgorithmsHelper.AlgorithmFilterStrings.ContainsKey((encodingOptions.DownMixStereoAlgorithm, DownMixAlgorithmsHelper.InferChannelLayout(state.AudioStream))); if (channels.HasValue && !useDownMixAlgorithm) @@ -7904,6 +7971,13 @@ namespace MediaBrowser.Controller.MediaEncoding audioTranscodeParams.Add("-ar " + sampleRateValue.ToString(CultureInfo.InvariantCulture)); } + // Without the downmix filter, -ac 2 alone drops the LFE channel. + var audioFilterParam = GetAudioFilterParam(state, encodingOptions); + if (!string.IsNullOrEmpty(audioFilterParam)) + { + audioTranscodeParams.Add(audioFilterParam.TrimStart()); + } + // Copy the movflags from GetProgressiveVideoFullCommandLine // See #9248 and the associated PR for why this is needed if (_mp4ContainerNames.Contains(state.OutputContainer)) diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs index 5045030b9b..12600af384 100644 --- a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Diagnostics; using System.Threading; using MediaBrowser.Model.Dto; @@ -14,6 +15,7 @@ public sealed class TranscodingJob : IDisposable private readonly ILogger<TranscodingJob> _logger; private readonly Lock _processLock = new(); private readonly Lock _timerLock = new(); + private readonly ConcurrentDictionary<int, long> _segmentEndPositionTicks = new(); private int _activeRequestCount; private Timer? _killTimer; @@ -157,6 +159,40 @@ public sealed class TranscodingJob : IDisposable public int PingTimeout { get; set; } /// <summary> + /// Records the end position of a served segment. + /// </summary> + /// <param name="segmentIndex">The segment index.</param> + /// <param name="segmentEndPositionTicks">The segment end position ticks.</param> + public void ReportSegmentDownloaded(int segmentIndex, long segmentEndPositionTicks) + { + if (segmentIndex < 0) + { + return; + } + + _segmentEndPositionTicks[segmentIndex] = segmentEndPositionTicks; + } + + /// <summary> + /// Gets the highest index among served segments whose end position is at or before the given position. + /// </summary> + /// <param name="positionTicks">The position ticks.</param> + /// <returns>The segment index, or <c>null</c> if there is none.</returns> + public int? GetHighestServedSegmentIndexEndingAtOrBefore(long positionTicks) + { + int? result = null; + foreach (var (index, endPositionTicks) in _segmentEndPositionTicks) + { + if (endPositionTicks <= positionTicks && (result is null || index > result)) + { + result = index; + } + } + + return result; + } + + /// <summary> /// Increments the active request count. /// </summary> /// <returns>The incremented count.</returns> diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs index 67bfcb02fd..924fc2e659 100644 --- a/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs @@ -22,7 +22,6 @@ public class TranscodingSegmentCleaner : IDisposable private readonly IFileSystem _fileSystem; private readonly IMediaEncoder _mediaEncoder; private Timer? _timer; - private int _segmentLength; /// <summary> /// Initializes a new instance of the <see cref="TranscodingSegmentCleaner"/> class. @@ -32,7 +31,7 @@ public class TranscodingSegmentCleaner : IDisposable /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="segmentLength">The segment length of this transcoding job.</param> + /// <param name="segmentLength">The segment length of this transcoding job. Unused.</param> public TranscodingSegmentCleaner(TranscodingJob job, ILogger<TranscodingSegmentCleaner> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder, int segmentLength) { _job = job; @@ -40,7 +39,6 @@ public class TranscodingSegmentCleaner : IDisposable _config = config; _fileSystem = fileSystem; _mediaEncoder = mediaEncoder; - _segmentLength = segmentLength; } /// <summary> @@ -104,11 +102,11 @@ public class TranscodingSegmentCleaner : IDisposable if (downloadPositionSeconds > 0 && segmentKeepSeconds > 0 && downloadPositionSeconds > segmentKeepSeconds) { - var idxMaxToDelete = (downloadPositionSeconds - segmentKeepSeconds) / _segmentLength; + var idxMaxToDelete = _job.GetHighestServedSegmentIndexEndingAtOrBefore(downloadPositionTicks - TimeSpan.FromSeconds(segmentKeepSeconds).Ticks); if (idxMaxToDelete > 0) { - await DeleteSegmentFiles(_job, 0, idxMaxToDelete, 1500).ConfigureAwait(false); + await DeleteSegmentFiles(_job, 0, idxMaxToDelete.Value, 1500).ConfigureAwait(false); } } } |
