diff --git a/src/nvenc/nvenc_base.cpp b/src/nvenc/nvenc_base.cpp index 13b2fa03a14..c2db4b9860c 100644 --- a/src/nvenc/nvenc_base.cpp +++ b/src/nvenc/nvenc_base.cpp @@ -222,13 +222,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 diff --git a/src/platform/linux/cuda.cpp b/src/platform/linux/cuda.cpp index 492adbd1238..205619bc614 100644 --- a/src/platform/linux/cuda.cpp +++ b/src/platform/linux/cuda.cpp @@ -805,7 +805,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}; diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index 11571c7c361..a91c17356ea 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -609,7 +609,7 @@ namespace platf { } 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; diff --git a/src/platform/linux/pipewire.cpp b/src/platform/linux/pipewire.cpp index d7a20940409..7a7f5db13a8 100644 --- a/src/platform/linux/pipewire.cpp +++ b/src/platform/linux/pipewire.cpp @@ -675,15 +675,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(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; diff --git a/src/platform/linux/wlgrab.cpp b/src/platform/linux/wlgrab.cpp index f99e81c65b4..dd84efb6d1e 100644 --- a/src/platform/linux/wlgrab.cpp +++ b/src/platform/linux/wlgrab.cpp @@ -30,15 +30,12 @@ namespace wl { public: 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(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; diff --git a/src/platform/linux/x11grab.cpp b/src/platform/linux/x11grab.cpp index 6eef5a81abc..1a42a07a9b0 100644 --- a/src/platform/linux/x11grab.cpp +++ b/src/platform/linux/x11grab.cpp @@ -391,7 +391,7 @@ namespace platf { return -1; } - delay = std::chrono::nanoseconds {1s} / config.framerate; + delay = ::video::capture_frame_interval(config); xwindow = DefaultRootWindow(xdisplay.get()); diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index e42bb8efa4b..7b7b3c05ef8 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -720,7 +720,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(fps.num), static_cast(fps.den)}; } diff --git a/src/video.cpp b/src/video.cpp index 0900d999de6..e83abd575c2 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -1678,13 +1678,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: diff --git a/src/video.h b/src/video.h index 5b474d3f101..8ba288c2eb6 100644 --- a/src/video.h +++ b/src/video.h @@ -4,6 +4,9 @@ */ #pragma once +// standard includes +#include + // local includes #include "input.h" #include "platform/common.h" @@ -386,4 +389,25 @@ 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. + */ + 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. + */ + inline std::chrono::nanoseconds capture_frame_interval(const config_t &config) { + const AVRational fps = framerate_to_rational(config); + return std::chrono::nanoseconds {(static_cast(fps.den) * 1'000'000'000LL) / fps.num}; + } } // namespace video diff --git a/tests/unit/test_video.cpp b/tests/unit/test_video.cpp index ed578f7d8db..e71fb1d8f6d 100644 --- a/tests/unit/test_video.cpp +++ b/tests/unit/test_video.cpp @@ -76,3 +76,48 @@ INSTANTIATE_TEST_SUITE_P( std::make_tuple(9498, AVRational {4749, 50}) // from my LG 27GN950 ) ); + +struct FramerateToRationalTest: testing::TestWithParam> {}; + +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> {}; + +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 + ) +);