diff --git a/docs/configuration.md b/docs/configuration.md
index 5ef85dc92..e9fe004dd 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -1753,6 +1753,67 @@ editing the `conf` file in a text editor. Use the examples as reference.
+### packetsize
+
+
+
+
Description
+
+ Limit the packetsize to avoid fragmentation on a low MTU link.
+ @note{This helps avoid packet loss and micro-stutter on a layer 2 VPN with
+ clients that cannot configure this value, e.g. Moonlight for Android/iOS.
+ }
+ @tip{To discover the optimal value:
+
+
Send ping to the server with don't fragment flag (DF)
+
Find the size of the largest replay, and subtract 16
+
Monitor the traffic to ensure no fragmentation
+
+ If using a VPN tunnel:
+
+
Set MTU on the TUN/TAP interface, and
+
Ensure no fragmentation both inside and outside the tunnel
+
Max UDP size = MTU size - 28
+
`packetsize` = max UDP size - 16
+
Monitor the traffic to ensure no fragmentation
+
+ Sample calculation for OpenVPN layer 2, using IPv4:
+
+
1428 bytes for max ICMP/UDP size outside the tunnel
+
Subtract the OpenVPN overhead: 24 bytes (may vary)
+
1404 bytes for Ethernet inside the tunnel
+
Subtract the Ethernet header: 14 bytes
+
1390 bytes for MTU inside the tunnel
+
Subtract the IPv4 header: 20 bytes
+
Subtract the UDP header: 8 bytes
+
1362 bytes for UDP payload
+
Subtract: 16 bytes
+
1346 bytes for `packetsize`
+
+ }
+ @warning{Reduce the bitrate when using low values.
+ Values larger than 1456 require jumbo frames.
+ }
+
+
+
+
Default
+
@code{}
+ 0
+ @endcode
+
+
+
Range
+
0, 200-65535
+
+
+
Example
+
@code{}
+ packetsize = 1346
+ @endcode
+
+
+
## Config Files
### file_apps
diff --git a/src/config.cpp b/src/config.cpp
index a79f7d881..6d266c0ef 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -544,6 +544,7 @@ namespace config {
ENCRYPTION_MODE_NEVER, // lan_encryption_mode
ENCRYPTION_MODE_OPPORTUNISTIC, // wan_encryption_mode
+ 0, // packetsize
};
nvhttp_t nvhttp {
@@ -1252,6 +1253,7 @@ namespace config {
int_between_f(vars, "lan_encryption_mode", stream.lan_encryption_mode, {0, 2});
int_between_f(vars, "wan_encryption_mode", stream.wan_encryption_mode, {0, 2});
+ int_between_f(vars, "packetsize", stream.packetsize, {0, PACKETSIZE_MAX});
path_f(vars, "file_apps", stream.file_apps);
#ifndef __ANDROID__
diff --git a/src/config.h b/src/config.h
index eb778a3ac..e0e40501d 100644
--- a/src/config.h
+++ b/src/config.h
@@ -16,6 +16,12 @@
#include "nvenc/nvenc_config.h"
namespace config {
+ // Valid range for the packetsize limit
+ constexpr int PACKETSIZE_MIN = 200;
+ constexpr int PACKETSIZE_MAX = 65535;
+ constexpr int PACKETSIZE_SMALL = 500;
+ constexpr int PACKETSIZE_LARGE = 1456;
+
// track modified config options
inline std::unordered_map modified_config_settings;
@@ -177,6 +183,9 @@ namespace config {
// Video encryption settings for LAN and WAN streams
int lan_encryption_mode;
int wan_encryption_mode;
+
+ // Limit the packetsize to avoid fragmentation on a low MTU link
+ int packetsize;
};
struct nvhttp_t {
diff --git a/src/rtsp.cpp b/src/rtsp.cpp
index 4e28df9e2..0953cd59f 100644
--- a/src/rtsp.cpp
+++ b/src/rtsp.cpp
@@ -1004,6 +1004,23 @@ namespace rtsp_stream {
config.encryptionFlagsEnabled |= SS_ENC_AUDIO;
}
+ // Limit the packetsize to avoid fragmentation with clients that cannot configure this value
+ if (config::stream.packetsize && config::stream.packetsize < config.packetsize) {
+ if (config::stream.packetsize < config::PACKETSIZE_MIN || config::stream.packetsize > config::PACKETSIZE_MAX) {
+ BOOST_LOG(warning) << "packetsize range: ["sv << config::PACKETSIZE_MIN << "-"sv << config::PACKETSIZE_MAX
+ << "] invalid value: "sv << config::stream.packetsize;
+ } else {
+ if (config::stream.packetsize < config::PACKETSIZE_SMALL) {
+ BOOST_LOG(info) << "packetsize is small < "sv << config::PACKETSIZE_SMALL << " bytes, reduce bitrate if the stream breaks"sv;
+ } else if (config::stream.packetsize > config::PACKETSIZE_LARGE) {
+ BOOST_LOG(info) << "packetsize is large > "sv << config::PACKETSIZE_LARGE << " bytes, jumbo frames may be used"sv;
+ }
+
+ BOOST_LOG(info) << "packetsize limit: "sv << config.packetsize << " -> "sv << config::stream.packetsize << " bytes"sv;
+ config.packetsize = config::stream.packetsize;
+ }
+ }
+
config.monitor.height = (int) util::from_view(args.at("x-nv-video[0].clientViewportHt"sv));
config.monitor.width = (int) util::from_view(args.at("x-nv-video[0].clientViewportWd"sv));
config.monitor.framerate = (int) util::from_view(args.at("x-nv-video[0].maxFPS"sv));
diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html
index 513932a2c..2e4ab7ca7 100644
--- a/src_assets/common/assets/web/config.html
+++ b/src_assets/common/assets/web/config.html
@@ -253,6 +253,7 @@
"lan_encryption_mode": 0,
"wan_encryption_mode": 1,
"ping_timeout": 10000,
+ "packetsize": 0,
},
},
{
diff --git a/src_assets/common/assets/web/configs/tabs/Network.vue b/src_assets/common/assets/web/configs/tabs/Network.vue
index da0a28de3..a89fc743f 100644
--- a/src_assets/common/assets/web/configs/tabs/Network.vue
+++ b/src_assets/common/assets/web/configs/tabs/Network.vue
@@ -173,6 +173,13 @@ const effectivePort = computed(() => +config.value?.port ?? defaultMoonlightPort
{{ $t('config.ping_timeout_desc') }}
+
+
+
+
+
{{ $t('config.packetsize_desc') }}
+
+
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json
index 1165ed886..5c3996ca2 100644
--- a/src_assets/common/assets/web/public/assets/locale/en.json
+++ b/src_assets/common/assets/web/public/assets/locale/en.json
@@ -319,6 +319,8 @@
"output_name": "Display Id",
"output_name_desc_unix": "During Sunshine startup, you should see the list of detected displays. Note: You need to use the id value inside the parenthesis. Below is an example; the actual output can be found in the Troubleshooting tab.",
"output_name_desc_windows": "Manually specify a display device id to use for capture. If unset, the primary display is captured. Note: If you specified a GPU above, this display must be connected to that GPU. During Sunshine startup, you should see the list of detected displays. Below is an example; the actual output can be found in the Troubleshooting tab.",
+ "packetsize": "Packet Size Limit",
+ "packetsize_desc": "Limit the packet size to avoid fragmentation on a low MTU link. This helps reduce packet loss and micro-stuttering, while streaming over a layer 2 VPN to clients that cannot configure this value, e.g. Moonlight for Android/iOS. Reduce the bitrate when using low values. Values larger than 1456 require jumbo frames. Range: 0, 200-65536. A value of 0 will disable the limit.",
"ping_timeout": "Ping Timeout",
"ping_timeout_desc": "How long to wait in milliseconds for data from moonlight before shutting down the stream",
"pkey": "Private Key",