diff --git a/src/nvenc/nvenc_base.cpp b/src/nvenc/nvenc_base.cpp index 431b7de2..c7ddb962 100644 --- a/src/nvenc/nvenc_base.cpp +++ b/src/nvenc/nvenc_base.cpp @@ -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))) { diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index d508ae97..a1f2b96f 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -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; diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 2538a690..d9da7f86 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -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(fps.num), static_cast(fps.den)}; + } + dxgi::output6_t output6 {}; status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6); if (SUCCEEDED(status)) { diff --git a/src/rtsp.cpp b/src/rtsp.cpp index 43117f37..f5909eb8 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -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)); diff --git a/src/video.cpp b/src/video.cpp index 8f6b69c4..bf10111d 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -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: diff --git a/src/video.h b/src/video.h index a966c53e..c4b9554f 100644 --- a/src/video.h +++ b/src/video.h @@ -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 diff --git a/tests/unit/test_video.cpp b/tests/unit/test_video.cpp index e8022d14..a55e9468 100644 --- a/tests/unit/test_video.cpp +++ b/tests/unit/test_video.cpp @@ -48,3 +48,31 @@ INSTANTIATE_TEST_SUITE_P( TEST_P(EncoderTest, ValidateEncoder) { // todo:: test something besides fixture setup } + +struct FramerateX100Test: testing::TestWithParam> {}; + +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 + ) +);