feat(linux): pace capture at exact fractional NTSC framerates (#5282)

This commit is contained in:
Aliaksei Dziadziuk
2026-06-30 15:40:08 +02:00
committed by GitHub
parent 9cb358952c
commit bba6c6cac0
10 changed files with 95 additions and 34 deletions
+3 -7
View File
@@ -226,13 +226,9 @@ namespace nvenc {
init_params.darWidth = encoder_params.width;
init_params.encodeHeight = encoder_params.height;
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;
}
const AVRational fps = video::framerate_to_rational(client_config);
init_params.frameRateNum = fps.num;
init_params.frameRateDen = fps.den;
if (client_config.videoFormat > 0 && get_encoder_cap(NV_ENC_CAPS_NUM_ENCODER_ENGINES) > 1) {
// SFE supports HEVC/AV1 if you have more than 1 nvenc block
+1 -1
View File
@@ -992,7 +992,7 @@ namespace cuda {
}
}
delay = std::chrono::nanoseconds {1s} / config.framerate;
delay = ::video::capture_frame_interval(config);
capture_params = NVFBC_CREATE_CAPTURE_SESSION_PARAMS {NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER};
+1 -1
View File
@@ -873,7 +873,7 @@ namespace platf {
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(const std::string &display_name, const ::video::config_t &config) {
delay = std::chrono::nanoseconds {1s} / config.framerate;
delay = ::video::capture_frame_interval(config);
int monitor_index = util::from_view(display_name);
int monitor = 0;
+5 -8
View File
@@ -806,15 +806,12 @@ namespace pipewire {
int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) {
// calculate frame interval we should capture at
framerate = config.framerate;
if (config.framerateX100 > 0) {
AVRational fps_strict = ::video::framerateX100_to_rational(config.framerateX100);
delay = std::chrono::nanoseconds(
(static_cast<int64_t>(fps_strict.den) * 1'000'000'000LL) / fps_strict.num
);
BOOST_LOG(info) << "[pipewire] Requested frame rate [" << fps_strict.num << "/" << fps_strict.den << ", approx. " << av_q2d(fps_strict) << " fps]";
delay = ::video::capture_frame_interval(config);
const AVRational fps = ::video::framerate_to_rational(config);
if (fps.den != 1) {
BOOST_LOG(info) << "[pipewire] Requested frame rate [" << fps.num << "/" << fps.den << ", approx. " << av_q2d(fps) << " fps]";
} else {
delay = std::chrono::nanoseconds {1s} / framerate;
BOOST_LOG(info) << "[pipewire] Requested frame rate [" << framerate << "fps]";
BOOST_LOG(info) << "[pipewire] Requested frame rate [" << fps.num << "fps]";
}
mem_type = hwdevice_type;
+5 -8
View File
@@ -47,15 +47,12 @@ namespace wl {
*/
int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) {
// calculate frame interval we should capture at
if (config.framerateX100 > 0) {
AVRational fps_strict = ::video::framerateX100_to_rational(config.framerateX100);
delay = std::chrono::nanoseconds(
(static_cast<int64_t>(fps_strict.den) * 1'000'000'000LL) / fps_strict.num
);
BOOST_LOG(info) << "[wlgrab] Requested frame rate [" << fps_strict.num << "/" << fps_strict.den << ", approx. " << av_q2d(fps_strict) << " fps]";
delay = ::video::capture_frame_interval(config);
const AVRational fps = ::video::framerate_to_rational(config);
if (fps.den != 1) {
BOOST_LOG(info) << "[wlgrab] Requested frame rate [" << fps.num << "/" << fps.den << ", approx. " << av_q2d(fps) << " fps]";
} else {
delay = std::chrono::nanoseconds {1s} / config.framerate;
BOOST_LOG(info) << "[wlgrab] Requested frame rate [" << config.framerate << "fps]";
BOOST_LOG(info) << "[wlgrab] Requested frame rate [" << fps.num << "fps]";
}
mem_type = hwdevice_type;
+1 -1
View File
@@ -499,7 +499,7 @@ namespace platf {
return -1;
}
delay = std::chrono::nanoseconds {1s} / config.framerate;
delay = ::video::capture_frame_interval(config);
xwindow = DefaultRootWindow(xdisplay.get());
+1 -1
View File
@@ -728,7 +728,7 @@ 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);
const AVRational fps = ::video::framerate_to_rational(config);
client_frame_rate_strict = DXGI_RATIONAL {static_cast<UINT>(fps.num), static_cast<UINT>(fps.den)};
}
+3 -7
View File
@@ -1954,13 +1954,9 @@ namespace video {
ctx.reset(avcodec_alloc_context3(codec));
ctx->width = config.width;
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};
}
const AVRational fps = video::framerate_to_rational(config);
ctx->framerate = fps;
ctx->time_base = AVRational {fps.den, fps.num};
switch (config.videoFormat) {
case 0:
+30
View File
@@ -4,6 +4,9 @@
*/
#pragma once
// standard includes
#include <chrono>
// local includes
#include "input.h"
#include "platform/common.h"
@@ -640,4 +643,31 @@ namespace video {
return av_d2q((double) framerateX100 / 100.0f, 1 << 26);
}
}
/**
* @brief Requested framerate as an exact rational.
* Uses the exact fractional rate when the client provided an X100 value,
* otherwise the integer framerate over 1.
*
* @param config Stream configuration carrying the requested framerate.
* @return Requested frame rate as a rational.
*/
inline AVRational framerate_to_rational(const config_t &config) {
if (config.framerateX100 > 0) {
return framerateX100_to_rational(config.framerateX100);
}
return AVRational {config.framerate, 1};
}
/**
* @brief Capture frame interval for the requested framerate.
* Uses the exact fractional rate when the client provided an X100 value.
*
* @param config Stream configuration carrying the requested framerate.
* @return Duration of a single frame interval in nanoseconds.
*/
inline std::chrono::nanoseconds capture_frame_interval(const config_t &config) {
const AVRational fps = framerate_to_rational(config);
return std::chrono::nanoseconds {(static_cast<int64_t>(fps.den) * 1'000'000'000LL) / fps.num};
}
} // namespace video
+45
View File
@@ -76,3 +76,48 @@ INSTANTIATE_TEST_SUITE_P(
std::make_tuple(9498, AVRational {4749, 50}) // from my LG 27GN950
)
);
struct FramerateToRationalTest: testing::TestWithParam<std::tuple<int, int, AVRational>> {};
TEST_P(FramerateToRationalTest, Run) {
const auto &[framerate, framerateX100, expected] = GetParam();
video::config_t config {};
config.framerate = framerate;
config.framerateX100 = framerateX100;
auto res = video::framerate_to_rational(config);
ASSERT_EQ(0, av_cmp_q(res, expected)) << "expected "
<< expected.num << "/" << expected.den
<< ", got "
<< res.num << "/" << res.den;
}
INSTANTIATE_TEST_SUITE_P(
FramerateToRationalTests,
FramerateToRationalTest,
testing::Values(
std::make_tuple(60, 0, AVRational {60, 1}), // no X100 value, fall back to integer framerate
std::make_tuple(60, 5994, AVRational {60000, 1001}),
std::make_tuple(120, 11988, AVRational {120000, 1001}),
std::make_tuple(24, 2398, AVRational {24000, 1001})
)
);
struct CaptureFrameIntervalTest: testing::TestWithParam<std::tuple<int, int, std::chrono::nanoseconds>> {};
TEST_P(CaptureFrameIntervalTest, Run) {
const auto &[framerate, framerateX100, expected] = GetParam();
video::config_t config {};
config.framerate = framerate;
config.framerateX100 = framerateX100;
ASSERT_EQ(expected, video::capture_frame_interval(config));
}
INSTANTIATE_TEST_SUITE_P(
CaptureFrameIntervalTests,
CaptureFrameIntervalTest,
testing::Values(
std::make_tuple(60, 0, std::chrono::nanoseconds {16666666}),
std::make_tuple(60, 5994, std::chrono::nanoseconds {16683333}), // 1e9 * 1001 / 60000
std::make_tuple(120, 11988, std::chrono::nanoseconds {8341666}) // 1e9 * 1001 / 120000
)
);