From 4030680fccdb6961dad149bfb2a2e02b7f5f0825 Mon Sep 17 00:00:00 2001 From: Yukino Song Date: Wed, 28 May 2025 20:32:56 +0800 Subject: [PATCH] App ordering support for legacy clients --- src/config.cpp | 2 + src/config.h | 1 + src/nvhttp.cpp | 19 +++++- src/zwpad.h | 61 +++++++++++++++++++ src_assets/common/assets/web/config.html | 2 + .../assets/web/configs/tabs/Advanced.vue | 11 +++- .../assets/web/public/assets/locale/en.json | 2 + .../assets/web/public/assets/locale/zh.json | 2 + 8 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 src/zwpad.h diff --git a/src/config.cpp b/src/config.cpp index b8b35264..bf3e7869 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -592,6 +592,7 @@ namespace config { "ipv4", // Address family platf::appdata().string() + "/sunshine.log", // log file false, // notify_pre_releases + false, // legacy_ordering {}, // prep commands {}, // server commands }; @@ -1284,6 +1285,7 @@ namespace config { bool_f(vars, "enable_discovery", sunshine.enable_discovery); bool_f(vars, "envvar_compatibility_mode", sunshine.envvar_compatibility_mode); bool_f(vars, "notify_pre_releases", sunshine.notify_pre_releases); + bool_f(vars, "legacy_ordering", sunshine.legacy_ordering); int port = sunshine.port; int_between_f(vars, "port"s, port, {1024 + nvhttp::PORT_HTTPS, 65535 - rtsp_stream::RTSP_SETUP_PORT}); diff --git a/src/config.h b/src/config.h index edb64f8f..c82393ec 100644 --- a/src/config.h +++ b/src/config.h @@ -280,6 +280,7 @@ namespace config { std::string log_file; bool notify_pre_releases; + bool legacy_ordering; std::vector prep_cmds; std::vector server_cmds; }; diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 3a7c0338..032f0232 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -36,6 +36,7 @@ #include "utility.h" #include "uuid.h" #include "video.h" +#include "zwpad.h" #ifdef _WIN32 #include "platform/windows/virtual_display.h" @@ -1069,7 +1070,16 @@ namespace nvhttp { if (!!(named_cert_p->perm & PERM::_all_actions)) { auto current_appid = proc::proc.running(); auto should_hide_inactive_apps = config::input.enable_input_only_mode && current_appid > 0 && current_appid != proc::input_only_app_id; - for (auto &app : proc::proc.get_apps()) { + + auto app_list = proc::proc.get_apps(); + + size_t bits; + if (config::sunshine.legacy_ordering) { + bits = zwpad::pad_width_for_count(app_list.size()); + } + + for (size_t i = 0; i < app_list.size(); i++) { + auto& app = app_list[i]; auto appid = util::from_view(app.id); if (should_hide_inactive_apps) { if ( @@ -1085,10 +1095,15 @@ namespace nvhttp { } } + auto app_name = app.name; + if (config::sunshine.legacy_ordering) { + zwpad::pad_for_ordering(app.name, bits, i); + } + pt::ptree app_node; app_node.put("IsHdrSupported"s, video::active_hevc_mode == 3 ? 1 : 0); - app_node.put("AppTitle"s, app.name); + app_node.put("AppTitle"s, app_name); app_node.put("UUID", app.uuid); app_node.put("IDX", app.idx); app_node.put("ID", app.id); diff --git a/src/zwpad.h b/src/zwpad.h new file mode 100644 index 00000000..28cbbf64 --- /dev/null +++ b/src/zwpad.h @@ -0,0 +1,61 @@ +// zero_width_pad.hpp +#pragma once +#include +#include // std::bit_width – C++20 +#include + +namespace zwpad +{ + // Two distinct zero-width characters. + // U+200B ZERO WIDTH SPACE – “0” + // U+200C ZERO WIDTH NON-JOINER – “1” + inline constexpr char8_t ZW0[] = u8"\u200B"; + inline constexpr char8_t ZW1[] = u8"\u200C"; + + /// \brief Encode \p index with a fixed-width binary prefix made of + /// zero-width code-points and append the original text. + /// + /// \param text The payload you actually want to keep visible. + /// \param padBits How many zero-width *digits* to prepend. + /// (Usually: std::bit_width(count-1).) + /// \param index Position in the ordered set, **0-based**. + /// + /// \return A UTF-8 std::string whose first \p padBits characters are + /// either U+200B or U+200C, followed by \p text. + /// + /// The lexical order of the resulting strings corresponds to the + /// numerical order of *index* because U+200B < U+200C. + /// + inline std::string + pad_for_ordering(std::string_view text, + std::size_t padBits, + std::size_t index) + { + if (padBits == 0) + throw std::invalid_argument("padBits must be > 0"); + if (index >= (std::size_t{1} << padBits)) + throw std::out_of_range("index does not fit into padBits"); + + std::string out; + out.reserve(padBits * 3 + text.size()); // each ZW char is 3-byte UTF-8 + + for (std::size_t bit = 0; bit < padBits; ++bit) + { + // Emit the *most* significant bit first. + const bool one = (index >> (padBits - 1 - bit)) & 1; + out += one ? reinterpret_cast(ZW1) + : reinterpret_cast(ZW0); + } + out.append(text); + return out; + } + + /// Convenience: compute the minimal pad width from the total count. + [[nodiscard]] + inline std::size_t pad_width_for_count(std::size_t count) + { + if (count == 0) + throw std::invalid_argument("count must be > 0"); + return std::bit_width(count - 1); // e.g. count==8 → 3 bits + } +} // namespace zwpad diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 91a64720..a4e3a868 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -242,6 +242,8 @@ "qp": 28, "min_threads": 2, "limit_framerate": "enabled", + "envvar_compatibility_mode": "disabled", + "legacy_ordering": "disabled", "hevc_mode": 0, "av1_mode": 0, "capture": "", diff --git a/src_assets/common/assets/web/configs/tabs/Advanced.vue b/src_assets/common/assets/web/configs/tabs/Advanced.vue index 60d57374..8e20ec11 100644 --- a/src_assets/common/assets/web/configs/tabs/Advanced.vue +++ b/src_assets/common/assets/web/configs/tabs/Advanced.vue @@ -5,8 +5,7 @@ import Checkbox from "../../Checkbox.vue"; const props = defineProps([ 'platform', - 'config', - 'global_prep_cmd' + 'config' ]) const config = ref(props.config) @@ -51,6 +50,14 @@ const config = ref(props.config) default="false" > + + +
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 f9510b72..96959f09 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -304,6 +304,8 @@ "lan_encryption_mode_1": "Enabled for supported clients", "lan_encryption_mode_2": "Required for all clients", "lan_encryption_mode_desc": "This determines when encryption will be used when streaming over your local network. Encryption can reduce streaming performance, particularly on less powerful hosts and clients.", + "legacy_ordering": "App ordering for legacy clients", + "legacy_ordering_desc": "Enable ordering support workaround for legacy clients. Can cause issues with clients or scripts that can't handle UTF8 correctly.", "limit_framerate": "Limit capture framerate", "limit_framerate_desc": "Limit the framerate being captured to client requested framerate. May not run at full framerate if vsync is enabled and display refreshrate does not match requested framerate. Could cause lag on some clients if disabled.", "locale": "Locale", diff --git a/src_assets/common/assets/web/public/assets/locale/zh.json b/src_assets/common/assets/web/public/assets/locale/zh.json index 7ad0c4ea..17897a16 100644 --- a/src_assets/common/assets/web/public/assets/locale/zh.json +++ b/src_assets/common/assets/web/public/assets/locale/zh.json @@ -299,6 +299,8 @@ "lan_encryption_mode_1": "为支持的客户端启用", "lan_encryption_mode_2": "强制所有客户端使用", "lan_encryption_mode_desc": "这将决定在本地网络上进行流媒体传输时何时使用加密。加密会降低流媒体性能,尤其是在功能较弱的主机和客户端上。", + "legacy_ordering": "过时客户端 APP 排序支持", + "legacy_ordering_desc": "启用对过时客户端的 APP 排序支持。可能在某些无法正确处理 UTF8 编码的客户端/脚本上导致问题。", "limit_framerate": "限制捕获帧率", "limit_framerate_desc": "将捕获帧率限制到客户端请求的帧率。当启用垂直同步且显示器刷新率与客户端刷新率不匹配时可能会跑不满请求的帧率。若禁用,可能会在某些客户端上导致延迟。", "locale": "本地化",