SHA256
Fix Encoder Quality Options Not Being Applied
This commit is contained in:
2 files changed
+110
-17
No files matched your search
+2
-1
@@ -47,4 +47,5 @@ desktop.ini
|
|||||||
/O5Theme.md
|
/O5Theme.md
|
||||||
|
|
||||||
# Developer Utility Files
|
# Developer Utility Files
|
||||||
/REVIEW_CHECKLIST.md
|
/REVIEW_CHECKLIST.md
|
||||||
|
/REVIEW_FFMPEG.md
|
||||||
@@ -83,7 +83,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
/// <summary>Frames per second (e.g., 60.0, 29.97, etc.).</summary>
|
/// <summary>Frames per second (e.g., 60.0, 29.97, etc.).</summary>
|
||||||
public double FPS;
|
public double FPS;
|
||||||
|
|
||||||
/// <summary>Video codec type to use (H264, H265, AV1, VP9).</summary>
|
/// <summary>Video codec type to use (H264, H265, AV1, VP9, VVC).</summary>
|
||||||
public Codec Codec;
|
public Codec Codec;
|
||||||
|
|
||||||
/// <summary>Bitrate control mode (CQP, CBR, VBR).</summary>
|
/// <summary>Bitrate control mode (CQP, CBR, VBR).</summary>
|
||||||
@@ -825,7 +825,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
/// Reference - CQP options per encoder:
|
/// Reference - CQP options per encoder:
|
||||||
/// NVENC: "qp" (single value applied to I/P/B frames)
|
/// NVENC: "qp" (single value applied to I/P/B frames)
|
||||||
/// AMF: "qp_i" + "qp_p" (I-frame and P-frame configured separately)
|
/// AMF: "qp_i" + "qp_p" (I-frame and P-frame configured separately)
|
||||||
/// QSV: "global_quality" (Intel unique option)
|
/// QSV: AVCodecContext.global_quality (Intel ICQ mode)
|
||||||
/// Software: "crf" (Constant Rate Factor, smarter quality control)
|
/// Software: "crf" (Constant Rate Factor, smarter quality control)
|
||||||
///
|
///
|
||||||
/// Reference: https://ffmpeg.org/doxygen/trunk/structAVCodecContext.html
|
/// Reference: https://ffmpeg.org/doxygen/trunk/structAVCodecContext.html
|
||||||
@@ -847,11 +847,21 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
_pCodecContext->rc_max_rate = _pCodecContext->rc_min_rate = _settings.TargetBitrate * 1000L;
|
_pCodecContext->rc_max_rate = _pCodecContext->rc_min_rate = _settings.TargetBitrate * 1000L;
|
||||||
_pCodecContext->rc_buffer_size = _settings.TargetBitrate * 1000;
|
_pCodecContext->rc_buffer_size = _settings.TargetBitrate * 1000;
|
||||||
|
|
||||||
// nal-hrd=cbr: x264/x265 dedicated option. Generates CBR bitstream matching
|
// HRD (Hypothetical Reference Decoder) signaling for CBR output, used for streaming compatibility.
|
||||||
// NAL HRD (Hypothetical Reference Decoder) specification. May be required for streaming compatibility.
|
// libx264: "nal-hrd=cbr" writes the NAL HRD information and pads filler data to keep the bitrate constant.
|
||||||
if (encoder == VideoEncoder.Libx264 || encoder == VideoEncoder.Libx265)
|
// FFmpeg's option description: "Signal HRD information (requires vbv-bufsize; cbr not allowed in .mp4)".
|
||||||
|
// Reference: https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/libx264.c ("nal-hrd": none/vbr/cbr)
|
||||||
|
// libx265: "x265-params" = "hrd=1" (--hrd: "Enable the signaling of HRD parameters to the decoder")
|
||||||
|
// + "strict-cbr=1" (--strict-cbr: "Enables stricter conditions to control bitrate deviance
|
||||||
|
// from the target bitrate in ABR mode").
|
||||||
|
// Reference: https://x265.readthedocs.io/en/master/cli.html
|
||||||
|
if (encoder == VideoEncoder.Libx264)
|
||||||
{
|
{
|
||||||
ffmpeg.av_opt_set(_pCodecContext->priv_data, "nal-hrd", "cbr", 0);
|
TrySetOption("nal-hrd", "cbr");
|
||||||
|
}
|
||||||
|
else if (encoder == VideoEncoder.Libx265)
|
||||||
|
{
|
||||||
|
TrySetOption("x265-params", "hrd=1:strict-cbr=1");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -875,38 +885,77 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case RateControlMode.CQP:
|
case RateControlMode.CQP:
|
||||||
|
// Valid quality range per encoder. The range of the private options is read from the
|
||||||
|
// encoder's option definition (AVOption min/max); a declared minimum of -1 means "auto".
|
||||||
|
// NVENC: "qp" H.264/HEVC 0~51, AV1 0~255
|
||||||
|
// (libavcodec/nvenc_h264.c, nvenc_hevc.c: "qp" -1~51 / libavcodec/nvenc_av1.c: "qp" -1~255)
|
||||||
|
// AMF: "qp_i" H.264/HEVC 0~51, AV1 0~255
|
||||||
|
// (libavcodec/amfenc_h264.c, amfenc_hevc.c: "qp_i" -1~51 / libavcodec/amfenc_av1.c: "qp_i" -1~255)
|
||||||
|
// QSV: AVCodecContext.global_quality 1~51
|
||||||
|
// VVC: "qp" 0~63 (libavcodec/libvvenc.c: "qp" -1~63)
|
||||||
|
// Software: "crf" x264/x265 0~51 (https://x265.readthedocs.io/en/master/cli.html: --crf 0..51.0),
|
||||||
|
// VP9/AV1 0~63 (libavcodec/libvpxenc.c, libaomenc.c: "crf" -1~63 / libavcodec/libsvtav1.c: "crf" 0~63)
|
||||||
|
// Reference: https://github.com/FFmpeg/FFmpeg/tree/master/libavcodec
|
||||||
|
int qpMin, qpMax;
|
||||||
|
if (encoder.Kind == EncoderKind.QSV)
|
||||||
|
{
|
||||||
|
// QSV runs in ICQ (Intelligent Constant Quality) mode when global_quality > 0 and rc_max_rate is 0;
|
||||||
|
// global_quality 0 selects the encoder's default CQP. The ICQ quality is clamped to 1~51 by FFmpeg:
|
||||||
|
// libavcodec/qsvenc.c: q->param.mfx.ICQQuality = av_clip(avctx->global_quality, 1, 51);
|
||||||
|
// Reference: https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/qsvenc.c
|
||||||
|
qpMin = 1;
|
||||||
|
qpMax = 51;
|
||||||
|
}
|
||||||
|
else if (encoder.Kind == EncoderKind.NVENC) (qpMin, qpMax) = GetQualityRange("qp");
|
||||||
|
else if (encoder.Kind == EncoderKind.AMF) (qpMin, qpMax) = GetQualityRange("qp_i");
|
||||||
|
else if (encoder == VideoEncoder.LibVvenc) (qpMin, qpMax) = GetQualityRange("qp");
|
||||||
|
else (qpMin, qpMax) = GetQualityRange("crf"); // Software encoder (libx264, libx265, libvpx-vp9, etc.)
|
||||||
|
|
||||||
// Default value 18: Quality close to "visually lossless" in H.264.
|
// Default value 18: Quality close to "visually lossless" in H.264.
|
||||||
// Provides similar level of quality in other codecs as well.
|
// Provides similar level of quality in other codecs as well.
|
||||||
var qpValue = _settings.QualityValue is >= 0 and <= 51 ? _settings.QualityValue : 18;
|
const int defaultQuality = 18;
|
||||||
|
var requestedQp = _settings.QualityValue;
|
||||||
|
int qpValue;
|
||||||
|
if (requestedQp < 0)
|
||||||
|
{
|
||||||
|
qpValue = Math.Clamp(defaultQuality, qpMin, qpMax);
|
||||||
|
Melon<RenderMod>.Logger.Warning($"{encoder.Name}: Invalid quality value {requestedQp}. Using {qpValue}.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qpValue = Math.Clamp(requestedQp, qpMin, qpMax);
|
||||||
|
if (qpValue != requestedQp)
|
||||||
|
Melon<RenderMod>.Logger.Warning($"{encoder.Name}: Quality value {requestedQp} is outside the supported range {qpMin}~{qpMax}. Using {qpValue}.");
|
||||||
|
}
|
||||||
|
|
||||||
if (encoder.Kind == EncoderKind.NVENC)
|
if (encoder.Kind == EncoderKind.NVENC)
|
||||||
{
|
{
|
||||||
// NVENC: Applies same QP to I/P/B frames with single "qp" option
|
// NVENC: Applies same QP to I/P/B frames with single "qp" option
|
||||||
ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp", qpValue.ToString(), 0);
|
TrySetOption("qp", qpValue.ToString());
|
||||||
}
|
}
|
||||||
else if (encoder.Kind == EncoderKind.AMF)
|
else if (encoder.Kind == EncoderKind.AMF)
|
||||||
{
|
{
|
||||||
// AMF: QP for I-frame (qp_i) and P-frame (qp_p) must be set separately.
|
// AMF: QP for I-frame (qp_i) and P-frame (qp_p) must be set separately.
|
||||||
// Since B-frames are disabled, qp_b is not set.
|
// Since B-frames are disabled, qp_b is not set.
|
||||||
ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp_i", qpValue.ToString(), 0);
|
TrySetOption("qp_i", qpValue.ToString());
|
||||||
ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp_p", qpValue.ToString(), 0);
|
TrySetOption("qp_p", qpValue.ToString());
|
||||||
}
|
}
|
||||||
else if (encoder.Kind == EncoderKind.QSV)
|
else if (encoder.Kind == EncoderKind.QSV)
|
||||||
{
|
{
|
||||||
// QSV: Uses ICQ (Intelligent Constant Quality) mode with "global_quality" option
|
// QSV: ICQ (Intelligent Constant Quality) mode, configured with AVCodecContext.global_quality.
|
||||||
ffmpeg.av_opt_set(_pCodecContext->priv_data, "global_quality", qpValue.ToString(), 0);
|
_pCodecContext->global_quality = qpValue;
|
||||||
}
|
}
|
||||||
else if (encoder == VideoEncoder.LibVvenc)
|
else if (encoder == VideoEncoder.LibVvenc)
|
||||||
{
|
{
|
||||||
// libvvenc: Specifies Constant QP mode with "qp" option
|
// libvvenc: Specifies Constant QP mode with "qp" option
|
||||||
ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp", qpValue.ToString(), 0);
|
TrySetOption("qp", qpValue.ToString());
|
||||||
}
|
}
|
||||||
else // Software encoder (libx264, libx265, libvpx-vp9, etc.)
|
else
|
||||||
{
|
{
|
||||||
// CRF (Constant Rate Factor): Smarter quality control than QP.
|
// CRF (Constant Rate Factor): Smarter quality control than QP.
|
||||||
// Automatically adjusts bitrate according to scene complexity to maintain consistent visual quality.
|
// Automatically adjusts bitrate according to scene complexity to maintain consistent visual quality.
|
||||||
// Range: 0 (lossless) ~ 51 (lowest quality). Typically between 18 and 28.
|
// Range: 0 (lossless) ~ 51 (lowest quality) for x264/x265, 0~63 for VP9/AV1. Typically between 18 and 28.
|
||||||
ffmpeg.av_opt_set(_pCodecContext->priv_data, "crf", qpValue.ToString(), 0);
|
TrySetOption("crf", qpValue.ToString());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -914,6 +963,49 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a private option (priv_data) of the encoder.
|
||||||
|
/// Returns true when FFmpeg accepts the option. Otherwise logs a warning with the FFmpeg error and returns false.
|
||||||
|
/// </summary>
|
||||||
|
private bool TrySetOption(string name, string value)
|
||||||
|
{
|
||||||
|
var ret = ffmpeg.av_opt_set(_pCodecContext->priv_data, name, value, 0);
|
||||||
|
if (ret >= 0) return true;
|
||||||
|
|
||||||
|
Melon<RenderMod>.Logger.Warning($"[Encoder] Option '{name}={value}' was not applied: {GetFFmpegErrorString(ret)} (Code: {ret})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Largest declared maximum that is treated as an actual quality range.
|
||||||
|
/// A declared maximum above this value is treated as unbounded, and the fallback range 0~51 is used.
|
||||||
|
/// The largest bounded quality range among the supported encoders is 0~255 (AV1 NVENC/AMF), while
|
||||||
|
/// libx264/libx265 declare "crf" as -1~FLT_MAX (libavcodec/libx264.c, libx265.c).
|
||||||
|
/// </summary>
|
||||||
|
private const double MaxMeaningfulQualityLimit = 1000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the valid range of a private quality option of the encoder from its option definition (AVOption min/max).
|
||||||
|
/// e.g. 0~51 for H.264/HEVC, 0~63 for VP9/AV1/VVC, 0~255 for AV1 NVENC/AMF.
|
||||||
|
/// The lower bound is at least 0 (a declared minimum of -1 means "auto").
|
||||||
|
/// When the option is missing or its declared maximum is unbounded (see <see cref="MaxMeaningfulQualityLimit"/>), returns 0~51,
|
||||||
|
/// the QP scale of H.264/HEVC 8-bit (x264/x265 CRF 0~51).
|
||||||
|
/// Reference: https://x265.readthedocs.io/en/master/cli.html (--crf 0..51.0)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="optionName">Name of the quality option in priv_data.</param>
|
||||||
|
private (int Min, int Max) GetQualityRange(string optionName)
|
||||||
|
{
|
||||||
|
const int fallbackMax = 51;
|
||||||
|
|
||||||
|
var option = ffmpeg.av_opt_find(_pCodecContext->priv_data, optionName, null, 0, 0);
|
||||||
|
if (option == null || !(option->max > 0 && option->max <= MaxMeaningfulQualityLimit))
|
||||||
|
return (0, fallbackMax);
|
||||||
|
|
||||||
|
var min = Math.Max(0, (int)Math.Ceiling(option->min));
|
||||||
|
var max = (int)Math.Floor(option->max);
|
||||||
|
return min <= max ? (min, max) : (0, fallbackMax);
|
||||||
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// Write Frame
|
// Write Frame
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|||||||
Reference in new issue
Block a user