ref:bba6c6cac0278af101a341fd90b59f11bb173ecb

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

SHA: bba6c6cac0278af101a341fd90b59f11bb173ecb
Author: Aliaksei Dziadziuk <djadjka.by@gmail.com>
Date: 2026-06-30 13:40
Parents: 9cb3589
10 files changed +95 -34
Type
src/nvenc/nvenc_base.cpp +3 −7
@@ -226,13 +226,9 @@
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
src/platform/linux/cuda.cpp +1 −1
@@ -992,6 +992,6 @@
}
}
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};
src/platform/linux/kmsgrab.cpp +1 −1
@@ -873,7 +873,7 @@
* @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;
src/platform/linux/pipewire.cpp +5 −8
@@ -806,14 +806,11 @@
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(
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]";
(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]";
} 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;
src/platform/linux/wlgrab.cpp +5 −8
@@ -47,15 +47,12 @@
*/
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(
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]";
(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]";
} 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;
src/platform/linux/x11grab.cpp +1 −1
@@ -499,6 +499,6 @@
return -1;
}
delay = std::chrono::nanoseconds {1s} / config.framerate;
delay = ::video::capture_frame_interval(config);
xwindow = DefaultRootWindow(xdisplay.get());
src/platform/windows/display_base.cpp +1 −1
@@ -728,6 +728,6 @@
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)};
}
src/video.cpp +3 −7
@@ -1954,13 +1954,9 @@
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:
src/video.h +30 −0
@@ -4,6 +4,9 @@
*/
#pragma once
// standard includes
#include <chrono>
// local includes
#include "input.h"
#include "platform/common.h"
@@ -639,5 +642,32 @@
// "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);
}
}
/**
* @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
tests/unit/test_video.cpp +45 −0
@@ -77,3 +77,48 @@
)
);
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
)
);