diff --git a/Services/Encoder/FfmpegArgs.cs b/Services/Encoder/FfmpegArgs.cs
index f5d79c3..fa6cd24 100644
--- a/Services/Encoder/FfmpegArgs.cs
+++ b/Services/Encoder/FfmpegArgs.cs
@@ -77,9 +77,29 @@ public static class FfmpegArgs
args.AddRange(["-vf", $"scale={options.Width}:{options.Height}:flags=bilinear,setsar=1"]);
}
args.AddRange(["-c:v", videoEncoder]);
+ AddRateControl(args, videoEncoder);
args.AddRange(["-b:v", $"{options.BitrateKbps}k", "-maxrate", $"{options.BitrateKbps}k",
"-bufsize", $"{options.BitrateKbps * 2}k"]);
args.AddRange(["-g", gop.ToString(), "-keyint_min", gop.ToString(),
"-sc_threshold", "0", "-bf", "0", "-pix_fmt", "yuv420p"]);
}
+
+ ///
+ /// Constant bitrate, spelled the one way each encoder understands.
+ ///
+ /// Why this is not a shared flag: the -b:v/-maxrate/-bufsize triad is capped VBR,
+ /// not CBR — measured 2026-11-12 on the bundled BtbN build, a static frame at -b:v 2000k
+ /// produced 9 kbps with NVENC's default rate control and 1951 kbps with -rc cbr.
+ /// That is the gap behind an @8 stream that averaged 5.1 Mbps.
+ ///
+ /// Per encoder: NVENC and AMF take an explicit mode. QSV has no -rc and already selects
+ /// CBR when bitrate == maxrate (libavcodec/qsvenc.c, select_rc_mode). OpenH264's
+ /// -rc_mode bitrate made no measurable difference on this build, so it is deliberately not
+ /// passed. YouTube's encoder spec requires CBR (support.google.com/youtube/answer/2853702).
+ ///
+ private static void AddRateControl(List args, string videoEncoder)
+ {
+ if (videoEncoder is "h264_nvenc" or "h264_amf")
+ args.AddRange(["-rc", "cbr"]);
+ }
}
diff --git a/docs/ai/output.md b/docs/ai/output.md
index d8ddb2e..b8cbf20 100644
--- a/docs/ai/output.md
+++ b/docs/ai/output.md
@@ -91,6 +91,13 @@ mixer writes IEEE-float stereo to a Windows named pipe** (`-f f32le -ar 48000 -a
anullsrc` silence in the TASK 8 audio milestone) + explicit `-map 0:v -map 1:a` + `-c:v -b:v K
-maxrate K -bufsize 2K` + **`-g fps×4 -keyint_min fps×4 -sc_threshold 0 -bf 0 -pix_fmt yuv420p`**
(≤4s keyframes, closed GOP, H.264 compliance) + `-c:a aac -ar 48000 -ac 2 -f flv `.
+**Rate control is CBR and it is per-encoder** (`FfmpegArgs.AddRateControl`). The `-b:v/-maxrate/-bufsize`
+triad is **capped VBR, not CBR**: measured 2026-11-12 on the bundled BtbN build, a static frame at
+`-b:v 2000k` produced **9 kbps** under NVENC's default and **1951 kbps** with `-rc cbr` — the cause of an
+@8 stream that averaged 5.1 Mbps. NVENC and AMF are passed `-rc cbr`; **QSV has no `-rc`** and selects CBR
+itself when `bitrate == maxrate` (`libavcodec/qsvenc.c`, `select_rc_mode`), so the triad is its whole ask;
+OpenH264's `-rc_mode bitrate` changed nothing measurable here and is not passed. YouTube's encoder spec
+requires CBR. Never assume `-maxrate` equals CBR — it does not.
**Record output (TASK 18):** `FfmpegArgs.Build` emits one self-contained block per output, each its own
`-map 0:v -map 1:a` + codec tags (`AddVideoTags` helper). Stream block = `-f flv `; record block =
`-f mp4 `. `EncoderOptions.StreamEnabled`/`RecordEnabled`/`RecordPath` gate each block, so
diff --git a/ytLive.Tests/FfmpegEncoderTests.cs b/ytLive.Tests/FfmpegEncoderTests.cs
index a930131..123f096 100644
--- a/ytLive.Tests/FfmpegEncoderTests.cs
+++ b/ytLive.Tests/FfmpegEncoderTests.cs
@@ -316,6 +316,33 @@ public class FfmpegEncoderTests
Assert.Equal("240", args[keyint + 1]);
}
+ [Fact]
+ public void Args_AskForConstantBitrateWhereTheEncoderNeedsTelling()
+ {
+ var options = new EncoderOptions { RtmpUrl = "rtmp://x/y", BitrateKbps = 12000 };
+
+ // Both hardware encoders own a rate-control switch; without it the target is an average to
+ // undershoot (measured: 9 kbps against a 2000k target on a static frame).
+ foreach (var encoder in new[] { "h264_nvenc", "h264_amf" })
+ {
+ var args = FfmpegArgs.Build(options, encoder).ToArray();
+ var rc = Array.IndexOf(args, "-rc");
+ Assert.True(rc >= 0, $"{encoder} must be told the rate-control mode");
+ Assert.Equal("cbr", args[rc + 1]);
+ }
+
+ // QSV has no -rc: libavcodec/qsvenc.c selects CBR when bitrate == maxrate, so the triad alone
+ // is the request. Passing -rc there would be an unknown option.
+ var qsv = FfmpegArgs.Build(options, "h264_qsv").ToArray();
+ Assert.DoesNotContain("-rc", qsv);
+ Assert.Equal(qsv[Array.IndexOf(qsv, "-b:v") + 1], qsv[Array.IndexOf(qsv, "-maxrate") + 1]);
+
+ // OpenH264 gains nothing measurable from -rc_mode bitrate; it stays out of the command line.
+ var openh264 = FfmpegArgs.Build(options, "libopenh264").ToArray();
+ Assert.DoesNotContain("-rc", openh264);
+ Assert.DoesNotContain("-rc_mode", openh264);
+ }
+
[Fact]
public void Args_IncludeInputOutputAndCompliance()
{