feat(fps): support x-nv-video[0].clientRefreshRateX100 for requesting fractional NTSC framerates (#4019)

This commit is contained in:
Andy Grundman 2025-10-11 19:56:12 -04:00 committed by GitHub
commit 6ed0c7a8f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 80 additions and 1 deletions

View file

@ -225,6 +225,11 @@ namespace nvenc {
init_params.darHeight = encoder_params.height;
init_params.frameRateNum = client_config.framerate;
init_params.frameRateDen = 1;
if (client_config.framerateX100 > 0) {
AVRational fps = video::framerateX100_to_rational(client_config.framerateX100);
init_params.frameRateNum = fps.num;
init_params.frameRateDen = fps.den;
}
NV_ENC_PRESET_CONFIG preset_config = {min_struct_version(NV_ENC_PRESET_CONFIG_VER), {min_struct_version(NV_ENC_CONFIG_VER, 7, 8)}};
if (nvenc_failed(nvenc->nvEncGetEncodePresetConfigEx(encoder, init_params.encodeGUID, init_params.presetGUID, init_params.tuningInfo, &preset_config))) {

View file

@ -173,6 +173,7 @@ namespace platf::dxgi {
int height_before_rotation;
int client_frame_rate;
DXGI_RATIONAL client_frame_rate_strict;
DXGI_FORMAT capture_format;
D3D_FEATURE_LEVEL feature_level;

View file

@ -121,7 +121,13 @@ namespace platf::dxgi {
display->display_refresh_rate = dup_desc.ModeDesc.RefreshRate;
double display_refresh_rate_decimal = (double) display->display_refresh_rate.Numerator / display->display_refresh_rate.Denominator;
BOOST_LOG(info) << "Display refresh rate [" << display_refresh_rate_decimal << "Hz]";
BOOST_LOG(info) << "Requested frame rate [" << display->client_frame_rate << "fps]";
if (display->client_frame_rate_strict.Numerator > 0) {
int num = display->client_frame_rate_strict.Numerator;
int den = display->client_frame_rate_strict.Denominator;
BOOST_LOG(info) << "Requested frame rate [" << num << "/" << den << " exactly " << av_q2d(AVRational {num, den}) << " fps]";
} else {
BOOST_LOG(info) << "Requested frame rate [" << display->client_frame_rate << "fps]";
}
display->display_refresh_rate_rounded = lround(display_refresh_rate_decimal);
return 0;
}
@ -196,6 +202,10 @@ namespace platf::dxgi {
capture_e display_base_t::capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) {
auto adjust_client_frame_rate = [&]() -> DXGI_RATIONAL {
// Use exactly the requested rate if the client sent an X100 value
if (client_frame_rate_strict.Numerator > 0) {
return client_frame_rate_strict;
}
// Adjust capture frame interval when display refresh rate is not integral but very close to requested fps.
if (display_refresh_rate.Denominator > 1) {
DXGI_RATIONAL candidate = display_refresh_rate;
@ -705,6 +715,12 @@ namespace platf::dxgi {
}
client_frame_rate = config.framerate;
client_frame_rate_strict = {0, 0};
if (config.framerateX100 > 0) {
AVRational fps = ::video::framerateX100_to_rational(config.framerateX100);
client_frame_rate_strict = DXGI_RATIONAL {static_cast<UINT>(fps.num), static_cast<UINT>(fps.den)};
}
dxgi::output6_t output6 {};
status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6);
if (SUCCEEDED(status)) {

View file

@ -952,6 +952,7 @@ namespace rtsp_stream {
args.try_emplace("x-ss-general.encryptionEnabled"sv, "0"sv);
args.try_emplace("x-ss-video[0].chromaSamplingType"sv, "0"sv);
args.try_emplace("x-ss-video[0].intraRefresh"sv, "0"sv);
args.try_emplace("x-nv-video[0].clientRefreshRateX100"sv, "0"sv);
stream::config_t config;
@ -981,6 +982,7 @@ namespace rtsp_stream {
config.monitor.height = util::from_view(args.at("x-nv-video[0].clientViewportHt"sv));
config.monitor.width = util::from_view(args.at("x-nv-video[0].clientViewportWd"sv));
config.monitor.framerate = util::from_view(args.at("x-nv-video[0].maxFPS"sv));
config.monitor.framerateX100 = util::from_view(args.at("x-nv-video[0].clientRefreshRateX100"sv));
config.monitor.bitrate = util::from_view(args.at("x-nv-vqos[0].bw.maximumBitrateKbps"sv));
config.monitor.slicesPerFrame = util::from_view(args.at("x-nv-video[0].videoEncoderSlicesPerFrame"sv));
config.monitor.numRefFrames = util::from_view(args.at("x-nv-video[0].maxNumReferenceFrames"sv));

View file

@ -1532,6 +1532,11 @@ namespace video {
ctx->height = config.height;
ctx->time_base = AVRational {1, config.framerate};
ctx->framerate = AVRational {config.framerate, 1};
if (config.framerateX100 > 0) {
AVRational fps = video::framerateX100_to_rational(config.framerateX100);
ctx->framerate = fps;
ctx->time_base = AVRational {fps.den, fps.num};
}
switch (config.videoFormat) {
case 0:

View file

@ -24,6 +24,7 @@ namespace video {
int width; // Video width in pixels
int height; // Video height in pixels
int framerate; // Requested framerate, used in individual frame bitrate budget calculation
int framerateX100; // Optional field for streaming at NTSC or similar rates e.g. 59.94 = 5994
int bitrate; // Video bitrate in kilobits (1000 bits) for requested framerate
int slicesPerFrame; // Number of slices per frame
int numRefFrames; // Max number of reference frames
@ -352,4 +353,25 @@ namespace video {
* @warning This is only safe to call when there is no client actively streaming.
*/
int probe_encoders();
// Several NTSC standard refresh rates are hardcoded here, because their
// true rate requires a denominator of 1001. ffmpeg's av_d2q() would assume it could
// reduce 29.97 to 2997/100 but this would be slightly wrong. We also include
// support for 23.976 film in case someone wants to stream a film at the perfect
// framerate.
inline AVRational framerateX100_to_rational(const int framerateX100) {
if (framerateX100 % 2997 == 0) {
// Multiples of NTSC 29.97 e.g. 59.94, 119.88
return AVRational {(framerateX100 / 2997) * 30000, 1001};
}
switch (framerateX100) {
case 2397: // the other weird NTSC framerate, assume these want 23.976 film
case 2398:
return AVRational {24000, 1001};
default:
// any other fractional rate can be reduced by ffmpeg. Max is set to 1 << 26 based on docs:
// "rational numbers with |num| <= 1<<26 && |den| <= 1<<26 can be recovered exactly from their double representation"
return av_d2q((double) framerateX100 / 100.0f, 1 << 26);
}
}
} // namespace video

View file

@ -48,3 +48,31 @@ INSTANTIATE_TEST_SUITE_P(
TEST_P(EncoderTest, ValidateEncoder) {
// todo:: test something besides fixture setup
}
struct FramerateX100Test: testing::TestWithParam<std::tuple<std::int32_t, AVRational>> {};
TEST_P(FramerateX100Test, Run) {
const auto &[x100, expected] = GetParam();
auto res = video::framerateX100_to_rational(x100);
ASSERT_EQ(0, av_cmp_q(res, expected)) << "expected "
<< expected.num << "/" << expected.den
<< ", got "
<< res.num << "/" << res.den;
}
INSTANTIATE_TEST_SUITE_P(
FramerateX100Tests,
FramerateX100Test,
testing::Values(
std::make_tuple(2397, AVRational {24000, 1001}),
std::make_tuple(2398, AVRational {24000, 1001}),
std::make_tuple(2500, AVRational {25, 1}),
std::make_tuple(2997, AVRational {30000, 1001}),
std::make_tuple(3000, AVRational {30, 1}),
std::make_tuple(5994, AVRational {60000, 1001}),
std::make_tuple(6000, AVRational {60, 1}),
std::make_tuple(11988, AVRational {120000, 1001}),
std::make_tuple(23976, AVRational {240000, 1001}), // future NTSC 240hz?
std::make_tuple(9498, AVRational {4749, 50}) // from my LG 27GN950
)
);