diff --git a/.gitignore b/.gitignore index 7a24da0..c911391 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,5 @@ desktop.ini /O5Theme.md # Developer Utility Files -/REVIEW_CHECKLIST.md \ No newline at end of file +/REVIEW_CHECKLIST.md +/REVIEW_FFMPEG.md \ No newline at end of file diff --git a/Util/FFmpeg/FFmpegNativeEncoder.cs b/Util/FFmpeg/FFmpegNativeEncoder.cs index 81efbbf..70ec1d4 100644 --- a/Util/FFmpeg/FFmpegNativeEncoder.cs +++ b/Util/FFmpeg/FFmpegNativeEncoder.cs @@ -83,7 +83,7 @@ namespace ADOFAIRenderer.Util.FFmpeg /// Frames per second (e.g., 60.0, 29.97, etc.). public double FPS; - /// Video codec type to use (H264, H265, AV1, VP9). + /// Video codec type to use (H264, H265, AV1, VP9, VVC). public Codec Codec; /// Bitrate control mode (CQP, CBR, VBR). @@ -825,7 +825,7 @@ namespace ADOFAIRenderer.Util.FFmpeg /// Reference - CQP options per encoder: /// NVENC: "qp" (single value applied to I/P/B frames) /// 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) /// /// 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_buffer_size = _settings.TargetBitrate * 1000; - // nal-hrd=cbr: x264/x265 dedicated option. Generates CBR bitstream matching - // NAL HRD (Hypothetical Reference Decoder) specification. May be required for streaming compatibility. - if (encoder == VideoEncoder.Libx264 || encoder == VideoEncoder.Libx265) + // HRD (Hypothetical Reference Decoder) signaling for CBR output, used for streaming compatibility. + // libx264: "nal-hrd=cbr" writes the NAL HRD information and pads filler data to keep the bitrate constant. + // 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; @@ -875,38 +885,77 @@ namespace ADOFAIRenderer.Util.FFmpeg break; 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. // 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.Logger.Warning($"{encoder.Name}: Invalid quality value {requestedQp}. Using {qpValue}."); + } + else + { + qpValue = Math.Clamp(requestedQp, qpMin, qpMax); + if (qpValue != requestedQp) + Melon.Logger.Warning($"{encoder.Name}: Quality value {requestedQp} is outside the supported range {qpMin}~{qpMax}. Using {qpValue}."); + } if (encoder.Kind == EncoderKind.NVENC) { // 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) { // 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. - ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp_i", qpValue.ToString(), 0); - ffmpeg.av_opt_set(_pCodecContext->priv_data, "qp_p", qpValue.ToString(), 0); + TrySetOption("qp_i", qpValue.ToString()); + TrySetOption("qp_p", qpValue.ToString()); } else if (encoder.Kind == EncoderKind.QSV) { - // QSV: Uses ICQ (Intelligent Constant Quality) mode with "global_quality" option - ffmpeg.av_opt_set(_pCodecContext->priv_data, "global_quality", qpValue.ToString(), 0); + // QSV: ICQ (Intelligent Constant Quality) mode, configured with AVCodecContext.global_quality. + _pCodecContext->global_quality = qpValue; } else if (encoder == VideoEncoder.LibVvenc) { // 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. // Automatically adjusts bitrate according to scene complexity to maintain consistent visual quality. - // Range: 0 (lossless) ~ 51 (lowest quality). Typically between 18 and 28. - ffmpeg.av_opt_set(_pCodecContext->priv_data, "crf", qpValue.ToString(), 0); + // Range: 0 (lossless) ~ 51 (lowest quality) for x264/x265, 0~63 for VP9/AV1. Typically between 18 and 28. + TrySetOption("crf", qpValue.ToString()); } break; default: @@ -914,6 +963,49 @@ namespace ADOFAIRenderer.Util.FFmpeg } } + /// + /// 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. + /// + 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.Logger.Warning($"[Encoder] Option '{name}={value}' was not applied: {GetFFmpegErrorString(ret)} (Code: {ret})"); + return false; + } + + /// + /// 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). + /// + private const double MaxMeaningfulQualityLimit = 1000; + + /// + /// 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 ), 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) + /// + /// Name of the quality option in priv_data. + 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 // ================================================================