aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller/MediaEncoding
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller/MediaEncoding')
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs98
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs36
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs8
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);
}
}
}