From 3a69acefa0e4c1a6b84c45ba3bc4c68323155693 Mon Sep 17 00:00:00 2001 From: Georgi Valkov <36749995+httpstorm@users.noreply.github.com> Date: Thu, 21 May 2026 18:43:53 +0300 Subject: [PATCH] feat(rtsp): add option to limit packetsize for clients that cannot configure it (#5153) Signed-off-by: Georgi Valkov --- docs/configuration.md | 61 +++++++++++++++++++ src/config.cpp | 2 + src/config.h | 9 +++ src/rtsp.cpp | 17 ++++++ src_assets/common/assets/web/config.html | 1 + .../assets/web/configs/tabs/Network.vue | 7 +++ .../assets/web/public/assets/locale/en.json | 2 + 7 files changed, 99 insertions(+) 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
Range0, 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",