Skip to content

Commit cf98e43

Browse files
committed
feat(fps): support x-nv-video[0].clientRefreshRateX100 for requesting fractional NTSC framerates
1 parent 65f14e1 commit cf98e43

File tree

9 files changed

+98
-3
lines changed

9 files changed

+98
-3
lines changed

src/nvenc/nvenc_base.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,11 @@ namespace nvenc {
222222
init_params.darHeight = encoder_params.height;
223223
init_params.frameRateNum = client_config.framerate;
224224
init_params.frameRateDen = 1;
225+
if (client_config.framerateX100 > 0) {
226+
AVRational fps = util::framerateX100_to_rational(client_config.framerateX100);
227+
init_params.frameRateNum = fps.num;
228+
init_params.frameRateDen = fps.den;
229+
}
225230

226231
NV_ENC_PRESET_CONFIG preset_config = {min_struct_version(NV_ENC_PRESET_CONFIG_VER), {min_struct_version(NV_ENC_CONFIG_VER, 7, 8)}};
227232
if (nvenc_failed(nvenc->nvEncGetEncodePresetConfigEx(encoder, init_params.encodeGUID, init_params.presetGUID, init_params.tuningInfo, &preset_config))) {

src/platform/windows/display.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ namespace platf::dxgi {
173173
int height_before_rotation;
174174

175175
int client_frame_rate;
176+
DXGI_RATIONAL client_frame_rate_strict;
176177

177178
DXGI_FORMAT capture_format;
178179
D3D_FEATURE_LEVEL feature_level;

src/platform/windows/display_base.cpp

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,13 @@ namespace platf::dxgi {
121121
display->display_refresh_rate = dup_desc.ModeDesc.RefreshRate;
122122
double display_refresh_rate_decimal = (double) display->display_refresh_rate.Numerator / display->display_refresh_rate.Denominator;
123123
BOOST_LOG(info) << "Display refresh rate [" << display_refresh_rate_decimal << "Hz]";
124-
BOOST_LOG(info) << "Requested frame rate [" << display->client_frame_rate << "fps]";
124+
if (display->client_frame_rate_strict.Numerator > 0) {
125+
int num = display->client_frame_rate_strict.Numerator;
126+
int den = display->client_frame_rate_strict.Denominator;
127+
BOOST_LOG(info) << "Requested frame rate [" << num << "/" << den << " exactly " << av_q2d(AVRational {num, den}) << " fps]";
128+
} else {
129+
BOOST_LOG(info) << "Requested frame rate [" << display->client_frame_rate << "fps]";
130+
}
125131
display->display_refresh_rate_rounded = lround(display_refresh_rate_decimal);
126132
return 0;
127133
}
@@ -196,6 +202,10 @@ namespace platf::dxgi {
196202

197203
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) {
198204
auto adjust_client_frame_rate = [&]() -> DXGI_RATIONAL {
205+
// Use exactly the requested rate if the client sent an X100 value
206+
if (client_frame_rate_strict.Numerator > 0) {
207+
return client_frame_rate_strict;
208+
}
199209
// Adjust capture frame interval when display refresh rate is not integral but very close to requested fps.
200210
if (display_refresh_rate.Denominator > 1) {
201211
DXGI_RATIONAL candidate = display_refresh_rate;
@@ -705,6 +715,12 @@ namespace platf::dxgi {
705715
}
706716

707717
client_frame_rate = config.framerate;
718+
client_frame_rate_strict = {0, 0};
719+
if (config.framerateX100 > 0) {
720+
AVRational fps = util::framerateX100_to_rational(config.framerateX100);
721+
client_frame_rate_strict = DXGI_RATIONAL {static_cast<UINT>(fps.num), static_cast<UINT>(fps.den)};
722+
}
723+
708724
dxgi::output6_t output6 {};
709725
status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6);
710726
if (SUCCEEDED(status)) {

src/rtsp.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,7 @@ namespace rtsp_stream {
933933
args.try_emplace("x-ss-general.encryptionEnabled"sv, "0"sv);
934934
args.try_emplace("x-ss-video[0].chromaSamplingType"sv, "0"sv);
935935
args.try_emplace("x-ss-video[0].intraRefresh"sv, "0"sv);
936+
args.try_emplace("x-nv-video[0].clientRefreshRateX100"sv, "0"sv);
936937

937938
stream::config_t config;
938939

@@ -962,6 +963,7 @@ namespace rtsp_stream {
962963
config.monitor.height = util::from_view(args.at("x-nv-video[0].clientViewportHt"sv));
963964
config.monitor.width = util::from_view(args.at("x-nv-video[0].clientViewportWd"sv));
964965
config.monitor.framerate = util::from_view(args.at("x-nv-video[0].maxFPS"sv));
966+
config.monitor.framerateX100 = util::from_view(args.at("x-nv-video[0].clientRefreshRateX100"sv));
965967
config.monitor.bitrate = util::from_view(args.at("x-nv-vqos[0].bw.maximumBitrateKbps"sv));
966968
config.monitor.slicesPerFrame = util::from_view(args.at("x-nv-video[0].videoEncoderSlicesPerFrame"sv));
967969
config.monitor.numRefFrames = util::from_view(args.at("x-nv-video[0].maxNumReferenceFrames"sv));

src/utility.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
*/
55
#pragma once
66

7+
extern "C" {
8+
#include "libavutil/rational.h"
9+
}
10+
711
// standard includes
812
#include <algorithm>
913
#include <condition_variable>
@@ -495,6 +499,27 @@ namespace util {
495499
return from_chars(std::begin(number), std::end(number));
496500
}
497501

502+
// Several NTSC standard refresh rates are hardcoded here, because their
503+
// true rate requires a denominator of 1001. ffmpeg's av_d2q() would assume it could
504+
// reduce 29.97 to 2997/100 but this would be slightly wrong. We also include
505+
// support for 23.976 film in case someone wants to stream a film at the perfect
506+
// framerate.
507+
inline AVRational framerateX100_to_rational(const int framerateX100) {
508+
switch (framerateX100) {
509+
case 11988:
510+
return AVRational {120000, 1001};
511+
case 5994:
512+
return AVRational {60000, 1001};
513+
case 2997:
514+
return AVRational {30000, 1001};
515+
case 2397: // assume these mean 23.976 film
516+
case 2398:
517+
return AVRational {24000, 1001};
518+
default:
519+
return av_d2q((double) framerateX100 / 100.0f, 1 << 26);
520+
}
521+
}
522+
498523
template<class X, class Y>
499524
class Either: public std::variant<std::monostate, X, Y> {
500525
public:

src/video.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,6 +1509,11 @@ namespace video {
15091509
ctx->height = config.height;
15101510
ctx->time_base = AVRational {1, config.framerate};
15111511
ctx->framerate = AVRational {config.framerate, 1};
1512+
if (config.framerateX100 > 0) {
1513+
AVRational fps = util::framerateX100_to_rational(config.framerateX100);
1514+
ctx->framerate = fps;
1515+
ctx->time_base = AVRational {fps.den, fps.num};
1516+
}
15121517

15131518
switch (config.videoFormat) {
15141519
case 0:

src/video.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ namespace video {
2424
int width; // Video width in pixels
2525
int height; // Video height in pixels
2626
int framerate; // Requested framerate, used in individual frame bitrate budget calculation
27+
int framerateX100; // Optional field for streaming at NTSC or similar rates e.g. 59.94 = 5994
2728
int bitrate; // Video bitrate in kilobits (1000 bits) for requested framerate
2829
int slicesPerFrame; // Number of slices per frame
2930
int numRefFrames; // Max number of reference frames

tests/unit/test_util.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* @file tests/unit/test_util.cpp
3+
* @brief Test functions in src/utility.h
4+
*/
5+
6+
extern "C" {
7+
#include "libavutil/rational.h"
8+
}
9+
10+
#include "../tests_common.h"
11+
12+
struct FramerateX100Test: testing::TestWithParam<std::tuple<std::int32_t, AVRational>> {};
13+
14+
TEST_P(FramerateX100Test, Run) {
15+
const auto &[x100, expected] = GetParam();
16+
auto res = util::framerateX100_to_rational(x100);
17+
ASSERT_EQ(0, av_cmp_q(res, expected)) << "expected "
18+
<< expected.num << "/" << expected.den
19+
<< ", got "
20+
<< res.num << "/" << res.den;
21+
}
22+
23+
INSTANTIATE_TEST_SUITE_P(
24+
FramerateX100Tests,
25+
FramerateX100Test,
26+
testing::Values(
27+
std::make_tuple(2397, AVRational {24000, 1001}),
28+
std::make_tuple(2398, AVRational {24000, 1001}),
29+
std::make_tuple(2500, AVRational {25, 1}),
30+
std::make_tuple(2997, AVRational {30000, 1001}),
31+
std::make_tuple(3000, AVRational {30, 1}),
32+
std::make_tuple(5994, AVRational {60000, 1001}),
33+
std::make_tuple(6000, AVRational {60, 1}),
34+
std::make_tuple(11988, AVRational {120000, 1001}),
35+
std::make_tuple(23976, AVRational {5994, 25})
36+
)
37+
);

tools/CMakeLists.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ cmake_minimum_required(VERSION 3.20)
22

33
project(sunshine_tools)
44

5-
include_directories("${CMAKE_SOURCE_DIR}")
5+
include_directories("${CMAKE_SOURCE_DIR}"
6+
${FFMPEG_INCLUDE_DIRS})
67

78
add_executable(dxgi-info dxgi.cpp)
89
set_target_properties(dxgi-info PROPERTIES CXX_STANDARD 20)
910
target_link_libraries(dxgi-info
1011
${CMAKE_THREAD_LIBS_INIT}
1112
dxgi
12-
${PLATFORM_LIBRARIES})
13+
${FFMPEG_LIBRARIES}
14+
${PLATFORM_LIBRARIES}
15+
bcrypt)
1316
target_compile_options(dxgi-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
1417

1518
add_executable(audio-info audio.cpp)

0 commit comments

Comments
 (0)