diff --git a/docs/configuration.md b/docs/configuration.md
index cf76e366..a6e5727c 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -1914,6 +1914,32 @@ consent prompt for directory ACLs, so the Web UI shows setup steps for correctin
+### dscp_tagging
+
+
+
+ | Description |
+
+ Mark audio and video packets with DSCP priority tags when requested by the client.
+ @tip{Disabling this may help if audio crackles or stutters on Wi-Fi while the network otherwise appears
+ healthy. Some routers handle tagged traffic poorly.}
+ @note{On Windows, disabling this also removes local traffic prioritization.}
+ |
+
+
+ | Default |
+ @code{}
+ enabled
+ @endcode |
+
+
+ | Example |
+ @code{}
+ dscp_tagging = disabled
+ @endcode |
+
+
+
## Config Files
### file_apps
diff --git a/src/config.cpp b/src/config.cpp
index 1e4300ba..42ae7dae 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -844,6 +844,7 @@ namespace config {
ENCRYPTION_MODE_NEVER, // lan_encryption_mode
ENCRYPTION_MODE_OPPORTUNISTIC, // wan_encryption_mode
0, // packetsize
+ true, // dscp_tagging
};
/**
@@ -1811,6 +1812,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});
+ bool_f(vars, "dscp_tagging", stream.dscp_tagging);
path_f(vars, "file_apps", stream.file_apps);
#ifndef __ANDROID__
diff --git a/src/config.h b/src/config.h
index d13ddfd4..80f3bd82 100644
--- a/src/config.h
+++ b/src/config.h
@@ -272,6 +272,8 @@ namespace config {
// Limit the packetsize to avoid fragmentation on a low MTU link
int packetsize; ///< Maximum payload size for network packets.
+
+ bool dscp_tagging; ///< Allow DSCP tagging of audio and video traffic when requested by the client.
};
/**
diff --git a/src/stream.cpp b/src/stream.cpp
index ab9125dc..28afa1c7 100644
--- a/src/stream.cpp
+++ b/src/stream.cpp
@@ -2227,7 +2227,7 @@ namespace stream {
// Enable local prioritization and QoS tagging on video traffic if requested by the client
auto address = session->video.peer.address();
- session->video.qos = platf::enable_socket_qos(ref->video_sock.native_handle(), address, session->video.peer.port(), platf::qos_data_type_e::video, session->config.videoQosType != 0);
+ session->video.qos = platf::enable_socket_qos(ref->video_sock.native_handle(), address, session->video.peer.port(), platf::qos_data_type_e::video, config::stream.dscp_tagging && session->config.videoQosType != 0);
BOOST_LOG(debug) << "Start capturing Video"sv;
video::capture(session->mail, session->config.monitor, session);
@@ -2254,7 +2254,7 @@ namespace stream {
// Enable local prioritization and QoS tagging on audio traffic if requested by the client
auto address = session->audio.peer.address();
- session->audio.qos = platf::enable_socket_qos(ref->audio_sock.native_handle(), address, session->audio.peer.port(), platf::qos_data_type_e::audio, session->config.audioQosType != 0);
+ session->audio.qos = platf::enable_socket_qos(ref->audio_sock.native_handle(), address, session->audio.peer.port(), platf::qos_data_type_e::audio, config::stream.dscp_tagging && session->config.audioQosType != 0);
BOOST_LOG(debug) << "Start capturing Audio"sv;
audio::capture(session->mail, session->config.audio, session);
diff --git a/src_assets/common/assets/web/configs/config_tabs.json b/src_assets/common/assets/web/configs/config_tabs.json
index 71ab0211..0b842314 100644
--- a/src_assets/common/assets/web/configs/config_tabs.json
+++ b/src_assets/common/assets/web/configs/config_tabs.json
@@ -77,7 +77,8 @@
"lan_encryption_mode": 0,
"wan_encryption_mode": 1,
"ping_timeout": 10000,
- "packetsize": 0
+ "packetsize": 0,
+ "dscp_tagging": "enabled"
}
},
{
diff --git a/src_assets/common/assets/web/configs/tabs/Network.vue b/src_assets/common/assets/web/configs/tabs/Network.vue
index 9988dcc7..d09cba9d 100644
--- a/src_assets/common/assets/web/configs/tabs/Network.vue
+++ b/src_assets/common/assets/web/configs/tabs/Network.vue
@@ -174,5 +174,13 @@ const effectivePort = computed(() => Number(config.value?.port ?? defaultMoonlig
{{ $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 416b274d..e631e94a 100644
--- a/src_assets/common/assets/web/public/assets/locale/en.json
+++ b/src_assets/common/assets/web/public/assets/locale/en.json
@@ -241,6 +241,8 @@
"dd_wa_hdr_toggle_delay": "High-contrast workaround for HDR",
"ds4_back_as_touchpad_click": "Map Back/Select to Touchpad Click",
"ds4_back_as_touchpad_click_desc": "When using PlayStation-style gamepad emulation, map Back/Select to Touchpad Click",
+ "dscp_tagging": "DSCP Tagging",
+ "dscp_tagging_desc": "Mark audio and video packets with DSCP priority tags when requested by the client. Disabling this may help if audio crackles or stutters on Wi-Fi, as some routers handle tagged traffic poorly.",
"virtualhid_randomize_mac": "Randomize virtual controller MAC",
"virtualhid_randomize_mac_desc": "Use a random MAC address for PlayStation-style virtual controllers instead of one based on the controller index. This avoids mixing per-controller settings when controllers are swapped on the client.",
"encoder": "Force a Specific Encoder",
diff --git a/tests/unit/test_config.cpp b/tests/unit/test_config.cpp
index 562137f0..5cff436c 100644
--- a/tests/unit/test_config.cpp
+++ b/tests/unit/test_config.cpp
@@ -220,6 +220,21 @@ TEST_F(ConfigPersistenceTest, ParsesFp16SdrGammaEncodedOption) {
EXPECT_FALSE(config::video.fp16_sdr_gamma_encoded);
}
+/**
+ * @brief Verify the DSCP tagging option parses enabled and disabled values.
+ */
+TEST_F(ConfigPersistenceTest, ParsesDscpTaggingOption) {
+ const auto apps_file = config_file().parent_path() / "sunshine_test_config_apps.json";
+ ASSERT_EQ(file_handler::write_file(apps_file.string().c_str(), "{}"), 0);
+ config::stream.file_apps = apps_file.string();
+
+ config::apply_config_for_test("dscp_tagging = disabled\n");
+ EXPECT_FALSE(config::stream.dscp_tagging);
+
+ config::apply_config_for_test("dscp_tagging = enabled\n");
+ EXPECT_TRUE(config::stream.dscp_tagging);
+}
+
/**
* @brief Verify mixed-case boolean values update configuration correctly.
*/
diff --git a/tests/web/Configuration.test.js b/tests/web/Configuration.test.js
index 9cc75518..04f7a2aa 100644
--- a/tests/web/Configuration.test.js
+++ b/tests/web/Configuration.test.js
@@ -103,7 +103,7 @@ describe('network configuration', () => {
const wrapper = mount(Network, {
props: {
platform: 'linux',
- config: { upnp: false },
+ config: { upnp: false, dscp_tagging: 'enabled' },
},
global: globalOptions,
})
@@ -114,6 +114,23 @@ describe('network configuration', () => {
expect(tableText).toContain('48010')
expect(tableText).not.toContain('NaN')
})
+
+ it('toggles DSCP tagging between enabled and disabled', async () => {
+ const config = { upnp: false, dscp_tagging: 'enabled' }
+ const wrapper = mount(Network, {
+ props: { platform: 'linux', config },
+ global: { ...globalOptions, stubs: { ...globalOptions.stubs, Checkbox: false } },
+ })
+ const input = wrapper.get('#dscp_tagging')
+
+ expect(input.element.checked).toBe(true)
+
+ await input.setValue(false)
+ expect(config.dscp_tagging).toBe('disabled')
+
+ await input.setValue(true)
+ expect(config.dscp_tagging).toBe('enabled')
+ })
})
describe('FEC configuration', () => {