From 8f1c2aa868630db76f865b296558b3d0215a398a Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Wed, 9 Sep 2026 13:28:09 +0200 Subject: [PATCH] fix(codec): set CRF via x264's private option, not global_quality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CRF value was set through AVCodecContext.global_quality with AV_CODEC_FLAG_QSCALE, which FFmpeg's libx264 wrapper divides by FF_QP2LAMBDA (118) before passing to x264 — turning CRF 16 into CRF 0.135 (essentially lossless) while x264 logged '-qscale is ignored, -crf is recommended' and fell back to its own defaults. The CRF was never actually applied. Now the CRF is set as x264's private "crf" option via av_opt_set, which passes the exact value directly to the encoder. The VBV max rate still caps bursts as before. --- src/codec/ffmpeg_encoder.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/codec/ffmpeg_encoder.cpp b/src/codec/ffmpeg_encoder.cpp index 84cb4fb..4f57727 100644 --- a/src/codec/ffmpeg_encoder.cpp +++ b/src/codec/ffmpeg_encoder.cpp @@ -417,7 +417,15 @@ CodecResult> EncoderFactory::create(const EncoderConfig // a fixed bitrate, and let the encoder use fewer bits on static screen // content and more on motion or text. The VBV max rate still caps the // peak so bursts cannot overflow the receiver's UDP buffers. - ctx->global_quality = static_cast(config.crf); + // CRF rate control: set x264's CRF directly as a private option. + // FFmpeg's global_quality + AV_CODEC_FLAG_QSCALE path divides by + // FF_QP2LAMBDA, giving a wrong CRF value (16/118 ≈ 0.1, essentially + // lossless) — hence the "-qscale is ignored" warning. Setting the + // private "crf" option bypasses that and gives x264 the exact value. + const std::string crf_value = std::to_string(config.crf); + if (av_opt_set(ctx->priv_data, "crf", crf_value.c_str(), 0) < 0) { + return CodecError{"failed to set CRF quality"}; + } ctx->rc_max_rate = static_cast(config.bitrate_kbps) * 1000; // VBV: one frame period of budget keeps bursts tight — a two-frame // buffer lets a keyframe spike beyond what a constrained link can