diff --git a/docs/configuration.md b/docs/configuration.md
index f788729d..a1ae900c 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -1270,7 +1270,7 @@ editing the `conf` file in a text editor. Use the examples as reference.
Remap the requested resolution and FPS to another display mode.
Depending on the [dd_resolution_option](#dd_resolution_option) and
- [dd_refresh_rate_option](#dd_refresh_rate_option) values, the following mapping
+ [dd_refresh_rate_option](#dd_refresh_rate_option) values, the following mapping
groups are available:
`mixed` - both options are set to `auto`.
@@ -1281,7 +1281,7 @@ editing the `conf` file in a text editor. Use the examples as reference.
`refresh_rate_only` - only [dd_refresh_rate_option](#dd_refresh_rate_option) is set to `auto`.
- For each of those groups, a list of fields can be configured to perform remapping:
+ For each of those groups, a list of fields can be configured to perform remapping:
`requested_resolution` - resolution that needs to be matched in order to use this remapping entry.
@@ -1291,10 +1291,10 @@ editing the `conf` file in a text editor. Use the examples as reference.
`final_refresh_rate` - refresh rate value to be used if the entry was matched.
If `requested_*` field is left empty, it will match everything.
- If `final_*` field is left empty, the original value will not be remapped and either a requested, manual
- or current value is used. However, at least one `final_*` must be set, otherwise the entry is considered
+ If `final_*` field is left empty, the original value will not be remapped and either a requested, manual
+ or current value is used. However, at least one `final_*` must be set, otherwise the entry is considered
invalid.
- @note{"Optimize game settings" must be enabled on client side for ANY entry with `resolution`
+ @note{"Optimize game settings" must be enabled on client side for ANY entry with `resolution`
field to be considered.}
@note{First entry to be matched in the list is the one that will be used.}
@tip{`requested_resolution` and `final_resolution` can be omitted for `refresh_rate_only` group.}
@@ -1371,6 +1371,32 @@ editing the `conf` file in a text editor. Use the examples as reference.
+### minimum_fps_target
+
+
+
+
Description
+
+ Sunshine tries to save bandwidth when content on screen is static or a low framerate. Because many clients expect a constant stream of video frames, a certain amount of duplicate frames are sent when this happens. This setting controls the lowest effective framerate a stream can reach.
+
+
+
+
Default
+
@code{}
+ 0
+ @endcode
+
+
+
Choices
+
0
+
Use half the stream's FPS as the minimum target.
+
+
+
1-1000
+
Specify your own value. The real minimum may differ from this value.
+
+
+
## Network
### upnp
diff --git a/src/config.cpp b/src/config.cpp
index c1dba388..17faacb4 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -504,7 +504,8 @@ namespace config {
{} // wa
}, // display_device
- 0 // max_bitrate
+ 0, // max_bitrate
+ 0 // minimum_fps_target (0 = framerate)
};
audio_t audio {
@@ -1146,6 +1147,7 @@ namespace config {
}
int_f(vars, "max_bitrate", video.max_bitrate);
+ double_between_f(vars, "minimum_fps_target", video.minimum_fps_target, {0.0, 1000.0});
path_f(vars, "pkey", nvhttp.pkey);
path_f(vars, "cert", nvhttp.cert);
diff --git a/src/config.h b/src/config.h
index 2e088ea7..287efaf5 100644
--- a/src/config.h
+++ b/src/config.h
@@ -141,6 +141,7 @@ namespace config {
} dd;
int max_bitrate; // Maximum bitrate, sets ceiling in kbps for bitrate requested from client
+ double minimum_fps_target; ///< Lowest framerate that will be used when streaming. Range 0-1000, 0 = half of client's requested framerate.
};
struct audio_t {
diff --git a/src/video.cpp b/src/video.cpp
index ed263e21..4db957b3 100644
--- a/src/video.cpp
+++ b/src/video.cpp
@@ -1890,9 +1890,10 @@ namespace video {
}
});
- // set minimum frame time based on client-requested target framerate
- std::chrono::duration minimum_frame_time {1000.0 / config.framerate};
- BOOST_LOG(info) << "Minimum frame time set to "sv << minimum_frame_time.count() << "ms, based on client-requested target framerate "sv << config.framerate << "."sv;
+ // set max frame time based on client-requested target framerate.
+ double minimum_fps_target = (config::video.minimum_fps_target > 0.0) ? config::video.minimum_fps_target : config.framerate;
+ std::chrono::duration max_frametime {1000.0 / minimum_fps_target};
+ BOOST_LOG(info) << "Minimum FPS target set to ~"sv << (minimum_fps_target / 2) << "fps ("sv << max_frametime.count() * 2 << "ms)"sv;
auto shutdown_event = mail->event(mail::shutdown);
auto packets = mail::man->queue(mail::video_packets);
@@ -1943,7 +1944,7 @@ namespace video {
// Encode at a minimum FPS to avoid image quality issues with static content
if (!requested_idr_frame || images->peek()) {
- if (auto img = images->pop(minimum_frame_time)) {
+ if (auto img = images->pop(max_frametime)) {
frame_timestamp = img->frame_timestamp;
if (session->convert(*img)) {
BOOST_LOG(error) << "Could not convert image"sv;
diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html
index 6e766e93..9c1fb5e9 100644
--- a/src_assets/common/assets/web/config.html
+++ b/src_assets/common/assets/web/config.html
@@ -179,6 +179,7 @@
"dd_mode_remapping": {"mixed": [], "resolution_only": [], "refresh_rate_only": []},
"dd_wa_hdr_toggle_delay": 0,
"max_bitrate": 0,
+ "minimum_fps_target": 0
},
},
{
diff --git a/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayModesSettings.vue b/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayModesSettings.vue
index 25b12deb..51b5b8c2 100644
--- a/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayModesSettings.vue
+++ b/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayModesSettings.vue
@@ -17,6 +17,13 @@ const config = ref(props.config)