[feat] add live remote video settings to the toolbar

This commit is contained in:
dijunkun
2026-09-24 15:44:02 +08:00
parent 88da1081ef
commit e0cd4c0567
18 changed files with 363 additions and 217 deletions
@@ -12,12 +12,6 @@ bool IsValidTurnModeValue(long value) {
value <= static_cast<long>(ConfigCenter::TURN_MODE::FORCE_TCP);
}
bool IsValidVideoAdaptationPolicyValue(long value) {
using Policy = ConfigCenter::VIDEO_ADAPTATION_POLICY;
return value >= static_cast<long>(Policy::FRAME_RATE_PRIORITY) &&
value <= static_cast<long>(Policy::BALANCED);
}
} // namespace
ConfigCenter::ConfigCenter(const std::string& config_path)
@@ -36,6 +30,11 @@ int ConfigCenter::Load() {
}
bool persist_config_migration = false;
// These options now belong only to the active remote connection. Discard
// legacy local defaults so they cannot affect this host or new connections.
persist_config_migration |= ini_.Delete(section_, "video_quality");
persist_config_migration |= ini_.Delete(section_, "video_frame_rate");
persist_config_migration |= ini_.Delete(section_, "video_adaptation_policy");
persist_config_migration |= ini_.Delete(section_, "video_content_type");
persist_config_migration |= ini_.Delete(section_, "screen_content");
persist_config_migration |=
@@ -52,12 +51,6 @@ int ConfigCenter::Load() {
language_ = static_cast<LANGUAGE>(language_value);
}
video_quality_ = static_cast<VIDEO_QUALITY>(ini_.GetLongValue(
section_, "video_quality", static_cast<long>(video_quality_)));
video_frame_rate_ = static_cast<VIDEO_FRAME_RATE>(ini_.GetLongValue(
section_, "video_frame_rate", static_cast<long>(video_frame_rate_)));
const long screen_capture_method_value = ini_.GetLongValue(
section_, "screen_capture_method",
static_cast<long>(ScreenCaptureMethod::Auto));
@@ -66,18 +59,6 @@ int ConfigCenter::Load() {
screen_capture_method_value)
: ScreenCaptureMethod::Auto;
const long video_adaptation_policy_value = ini_.GetLongValue(
section_, "video_adaptation_policy",
static_cast<long>(video_adaptation_policy_));
if (IsValidVideoAdaptationPolicyValue(video_adaptation_policy_value)) {
video_adaptation_policy_ =
static_cast<VIDEO_ADAPTATION_POLICY>(video_adaptation_policy_value);
} else {
LOG_WARN("Invalid video adaptation policy [{}], using quality priority",
video_adaptation_policy_value);
video_adaptation_policy_ = VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY;
}
video_encode_format_ = static_cast<VIDEO_ENCODE_FORMAT>(
ini_.GetLongValue(section_, "video_encode_format",
static_cast<long>(video_encode_format_)));
@@ -156,14 +137,8 @@ int ConfigCenter::Load() {
int ConfigCenter::Save() {
ini_.SetLongValue(section_, "language", static_cast<long>(language_));
ini_.SetLongValue(section_, "video_quality",
static_cast<long>(video_quality_));
ini_.SetLongValue(section_, "video_frame_rate",
static_cast<long>(video_frame_rate_));
ini_.SetLongValue(section_, "screen_capture_method",
static_cast<long>(screen_capture_method_));
ini_.SetLongValue(section_, "video_adaptation_policy",
static_cast<long>(video_adaptation_policy_));
ini_.SetLongValue(section_, "video_encode_format",
static_cast<long>(video_encode_format_));
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
@@ -210,28 +185,6 @@ int ConfigCenter::SetLanguage(LANGUAGE language) {
return 0;
}
int ConfigCenter::SetVideoQuality(VIDEO_QUALITY video_quality) {
video_quality_ = video_quality;
ini_.SetLongValue(section_, "video_quality",
static_cast<long>(video_quality_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate) {
video_frame_rate_ = video_frame_rate;
ini_.SetLongValue(section_, "video_frame_rate",
static_cast<long>(video_frame_rate_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetScreenCaptureMethod(ScreenCaptureMethod method) {
if (!IsValidScreenCaptureMethod(static_cast<long>(method))) {
return -1;
@@ -246,18 +199,6 @@ int ConfigCenter::SetScreenCaptureMethod(ScreenCaptureMethod method) {
return 0;
}
int ConfigCenter::SetVideoAdaptationPolicy(
VIDEO_ADAPTATION_POLICY policy) {
video_adaptation_policy_ = policy;
ini_.SetLongValue(section_, "video_adaptation_policy",
static_cast<long>(video_adaptation_policy_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetVideoEncodeFormat(
VIDEO_ENCODE_FORMAT video_encode_format) {
video_encode_format_ = video_encode_format;
@@ -426,23 +367,10 @@ int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) {
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
ConfigCenter::VIDEO_QUALITY ConfigCenter::GetVideoQuality() const {
return video_quality_;
}
ConfigCenter::VIDEO_FRAME_RATE ConfigCenter::GetVideoFrameRate() const {
return video_frame_rate_;
}
ScreenCaptureMethod ConfigCenter::GetScreenCaptureMethod() const {
return screen_capture_method_;
}
ConfigCenter::VIDEO_ADAPTATION_POLICY
ConfigCenter::GetVideoAdaptationPolicy() const {
return video_adaptation_policy_;
}
ConfigCenter::VIDEO_ENCODE_FORMAT ConfigCenter::GetVideoEncodeFormat() const {
return video_encode_format_;
}
@@ -18,13 +18,6 @@ namespace crossdesk {
class ConfigCenter {
public:
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 };
enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 };
enum class VIDEO_ADAPTATION_POLICY {
FRAME_RATE_PRIORITY = 0,
QUALITY_PRIORITY = 1,
BALANCED = 2
};
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
enum class TURN_MODE {
DISABLED = 0,
@@ -39,10 +32,7 @@ class ConfigCenter {
// write config
int SetLanguage(LANGUAGE language);
int SetVideoQuality(VIDEO_QUALITY video_quality);
int SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate);
int SetScreenCaptureMethod(ScreenCaptureMethod method);
int SetVideoAdaptationPolicy(VIDEO_ADAPTATION_POLICY policy);
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
int SetHardwareVideoCodec(bool hardware_video_codec);
int SetTurnMode(TURN_MODE turn_mode);
@@ -59,10 +49,7 @@ class ConfigCenter {
// read config
LANGUAGE GetLanguage() const;
VIDEO_QUALITY GetVideoQuality() const;
VIDEO_FRAME_RATE GetVideoFrameRate() const;
ScreenCaptureMethod GetScreenCaptureMethod() const;
VIDEO_ADAPTATION_POLICY GetVideoAdaptationPolicy() const;
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
// Whether this build includes a hardware video codec backend.
static bool IsHardwareVideoCodecAvailable();
@@ -91,11 +78,7 @@ class ConfigCenter {
const char* section_ = "Settings";
LANGUAGE language_ = LANGUAGE::CHINESE;
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::HIGH;
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
ScreenCaptureMethod screen_capture_method_ = ScreenCaptureMethod::Auto;
VIDEO_ADAPTATION_POLICY video_adaptation_policy_ =
VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY;
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
bool hardware_video_codec_ = false;
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
@@ -1250,10 +1250,6 @@ void GuiApplication::ResetSettingsUi() {
return;
}
ui_->main->set_language_index(language_button_value_);
ui_->main->set_video_quality_index(video_quality_button_value_);
ui_->main->set_frame_rate_index(video_frame_rate_button_value_);
ui_->main->set_adaptation_policy_index(
video_adaptation_policy_button_value_);
ui_->main->set_codec_index(video_encode_format_button_value_);
const bool hardware_codec_available =
ConfigCenter::IsHardwareVideoCodecAvailable();
@@ -1623,6 +1619,32 @@ void GuiApplication::BindStreamCallbacks() {
keyboard_.ForceReleasePressedKeys();
}
});
stream->on_change_video_settings([this](int quality, int frame_rate_index,
int preference) {
auto props = SelectedSession();
if (!props || !props->peer_ ||
props->connection_status_.load() != ConnectionStatus::Connected)
return;
RemoteAction action{};
action.type = ControlType::video_settings;
action.vs = {quality, frame_rate_index == 0 ? 30 : 60, preference};
if (frame_rate_index < 0 || frame_rate_index > 1 ||
!ValidVideoSettings(action.vs))
return;
{
std::lock_guard lock(props->video_settings_mutex_);
if (!props->video_settings_supported_) return;
action.vs.request_id = props->video_settings_.request_id + 1;
const auto message = action.to_json();
if (SendReliableDataFrame(props->peer_, message.data(), message.size(),
props->control_data_label_.c_str()) != 0)
return;
props->video_settings_ = action.vs;
props->video_settings_pending_ = true;
props->video_settings_failed_ = false;
props->video_settings_request_tick_ = SDL_GetTicks();
}
});
stream->on_toggle_audio([this] {
auto props = SelectedSession();
if (!props || !props->connection_established_) {
@@ -2672,6 +2694,33 @@ void GuiApplication::SyncStreamWindow() {
(*ui_->stream)->set_remote_cursor_active(remote_cursor_active);
(*ui_->stream)->set_remote_cursor_shape(remote_cursor_shape);
(*ui_->stream)->set_audio_enabled(props->audio_capture_button_pressed_);
{
std::lock_guard lock(props->video_settings_mutex_);
if (props->video_settings_pending_ &&
SDL_GetTicks() - props->video_settings_request_tick_ > 5000) {
props->video_settings_pending_ = false;
props->video_settings_failed_ = true;
const auto request_id = props->video_settings_.request_id;
props->video_settings_ = props->applied_video_settings_;
props->video_settings_.request_id = request_id;
}
(*ui_->stream)
->set_video_settings_enabled(status == ConnectionStatus::Connected &&
props->video_settings_supported_);
(*ui_->stream)
->set_video_settings_feedback(UiText(
props->video_settings_failed_ ? localization::video_settings_failed
[localization_language_index_]
: props->video_settings_pending_
? localization::video_settings_pending
[localization_language_index_]
: ""));
(*ui_->stream)->set_video_quality_index(props->video_settings_.quality);
(*ui_->stream)
->set_frame_rate_index(props->video_settings_.frame_rate == 30 ? 0 : 1);
(*ui_->stream)
->set_adaptation_policy_index(props->video_settings_.preference);
}
#if defined(__APPLE__)
fullscreen_button_pressed_ = IsStreamWindowFullscreen();
#else
@@ -3062,12 +3111,6 @@ void GuiApplication::SaveSettingsFromUi() {
auto& main = ui_->main;
language_button_value_ =
localization::detail::ClampLanguageIndex(main->get_language_index());
video_quality_button_value_ =
std::clamp(main->get_video_quality_index(), 0, 2);
video_frame_rate_button_value_ =
std::clamp(main->get_frame_rate_index(), 0, 1);
video_adaptation_policy_button_value_ =
std::clamp(main->get_adaptation_policy_index(), 0, 2);
video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1);
enable_hardware_video_codec_ = ConfigCenter::IsHardwareVideoCodecAvailable() &&
main->get_hardware_codec_enabled();
@@ -3080,15 +3123,8 @@ void GuiApplication::SaveSettingsFromUi() {
static_cast<ConfigCenter::LANGUAGE>(language_button_value_);
localization_language_index_ = language_button_value_;
config_center_->SetLanguage(localization_language_);
privacy_.SetText({
localization::privacy_screen_unlock_hint[localization_language_index_]});
config_center_->SetVideoQuality(
static_cast<ConfigCenter::VIDEO_QUALITY>(video_quality_button_value_));
config_center_->SetVideoFrameRate(static_cast<ConfigCenter::VIDEO_FRAME_RATE>(
video_frame_rate_button_value_));
config_center_->SetVideoAdaptationPolicy(
static_cast<ConfigCenter::VIDEO_ADAPTATION_POLICY>(
video_adaptation_policy_button_value_));
privacy_.SetText(
{localization::privacy_screen_unlock_hint[localization_language_index_]});
config_center_->SetVideoEncodeFormat(
static_cast<ConfigCenter::VIDEO_ENCODE_FORMAT>(
video_encode_format_button_value_));
@@ -3133,10 +3169,6 @@ void GuiApplication::SaveSettingsFromUi() {
}
language_button_value_last_ = language_button_value_;
video_quality_button_value_last_ = video_quality_button_value_;
video_frame_rate_button_value_last_ = video_frame_rate_button_value_;
video_adaptation_policy_button_value_last_ =
video_adaptation_policy_button_value_;
video_encode_format_button_value_last_ = video_encode_format_button_value_;
enable_hardware_video_codec_last_ = enable_hardware_video_codec_;
enable_turn_last_ = enable_turn_;
@@ -131,6 +131,10 @@ struct TranslationRow {
X(screen_capture_method, u8"采集方式:", "Capture Method:", \
u8"Способ захвата:") \
X(screen_capture_method_auto, u8"自动", "Auto", u8"Авто") \
X(video_settings, u8"画面设置", "Video settings", u8"Настройки видео") \
X(video_settings_pending, u8"正在应用…", "Applying…", u8"Применение…") \
X(video_settings_failed, u8"调整失败,请重试。", "Could not apply settings. Try again.", u8"Не удалось применить настройки. Повторите попытку.") \
X(video_settings_unavailable, u8"连接建立后可调整,需被控端支持。", "Available when connected to a supported remote device.", u8"Доступно при подключении к поддерживаемому устройству.") \
X(video_quality, u8"画面质量:", "Video Quality:", u8"Качество видео:") \
X(video_frame_rate, u8"画面采集帧率:", \
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
@@ -142,10 +142,10 @@ int SessionDeviceManager::InitializeScreenCapturer() {
capture_secure_callbacks_ = 0;
capture_send_errors_ = 0;
capture_max_gap_us_ = capture_send_us_ = capture_max_send_us_ = 0;
const int fps = owner_.config_center_->GetVideoFrameRate() ==
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
? 30
: 60;
// Keep the shared source ready for either live rate. MiniRTC admits 30/60
// frames independently per viewer, so switching never restarts native
// capture.
const int fps = 60;
LOG_INFO("Init screen capturer with {} fps", fps);
SetDisplayInfoList({});
registered_display_stream_count_ = 0;
@@ -297,12 +297,6 @@ int SettingsManager::Load() {
owner_.language_button_value_ = localization::detail::ClampLanguageIndex(
static_cast<int>(owner_.config_center_->GetLanguage()));
owner_.video_quality_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoQuality());
owner_.video_frame_rate_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoFrameRate());
owner_.video_adaptation_policy_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoAdaptationPolicy());
owner_.video_encode_format_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoEncodeFormat());
owner_.enable_hardware_video_codec_ =
@@ -324,11 +318,6 @@ int SettingsManager::Load() {
owner_.file_transfer_save_path_last_ = saved_path;
owner_.language_button_value_last_ = owner_.language_button_value_;
owner_.video_quality_button_value_last_ = owner_.video_quality_button_value_;
owner_.video_frame_rate_button_value_last_ =
owner_.video_frame_rate_button_value_;
owner_.video_adaptation_policy_button_value_last_ =
owner_.video_adaptation_policy_button_value_;
owner_.video_encode_format_button_value_last_ =
owner_.video_encode_format_button_value_;
owner_.enable_hardware_video_codec_last_ =
@@ -58,8 +58,9 @@ private:
struct CacheV1 {
char client_id_with_password[17];
int language;
int video_quality;
int video_frame_rate;
// Reserved legacy slots: retain the on-disk cache layout.
int legacy_video_quality;
int legacy_video_frame_rate;
int video_encode_format;
bool enable_hardware_video_codec;
bool enable_turn;
@@ -71,8 +72,9 @@ private:
struct CacheV2 {
char client_id_with_password[17];
int language;
int video_quality;
int video_frame_rate;
// Reserved legacy slots: retain the on-disk cache layout.
int legacy_video_quality;
int legacy_video_frame_rate;
int video_encode_format;
bool enable_hardware_video_codec;
bool enable_turn;
+6 -22
View File
@@ -136,28 +136,12 @@ int GuiRuntime::CreateConnectionPeer() {
params_.turn_mode = static_cast<TurnMode>(config_center_->GetTurnMode());
params_.enable_srtp = config_center_->IsEnableSrtp();
params_.video_content_type = VideoContentType::ScreenContent;
params_.video_quality =
static_cast<VideoQuality>(config_center_->GetVideoQuality());
params_.video_frame_rate =
config_center_->GetVideoFrameRate() ==
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
? 30
: 60;
switch (config_center_->GetVideoAdaptationPolicy()) {
case ConfigCenter::VIDEO_ADAPTATION_POLICY::FRAME_RATE_PRIORITY:
params_.video_degradation_preference =
VideoDegradationPreference::MaintainFrameRate;
break;
case ConfigCenter::VIDEO_ADAPTATION_POLICY::BALANCED:
params_.video_degradation_preference =
VideoDegradationPreference::Balanced;
break;
case ConfigCenter::VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY:
default:
params_.video_degradation_preference =
VideoDegradationPreference::MaintainResolution;
break;
}
// Bootstrap outgoing media with fixed defaults. Live overrides come only
// from the authorized controller and apply to its connection on this host.
params_.video_quality = VideoQuality::QualityHigh;
params_.video_frame_rate = 60;
params_.video_degradation_preference =
VideoDegradationPreference::MaintainResolution;
params_.on_receive_video_buffer = nullptr;
params_.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer;
params_.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer;
@@ -146,6 +146,47 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
std::string remote_id(user_id, user_id_size);
if (remote_action.type == ControlType::video_settings_status) {
if (auto props = runtime->FindRemoteSession(remote_id)) {
if (source_id != props->control_data_label_) return;
std::lock_guard lock(props->video_settings_mutex_);
if (!props->video_settings_supported_) return;
if (remote_action.vs.accepted &&
static_cast<int32_t>(remote_action.vs.request_id -
props->applied_video_settings_.request_id) >= 0) {
props->applied_video_settings_ = remote_action.vs;
}
if (remote_action.vs.request_id != props->video_settings_.request_id) return;
props->video_settings_pending_ = false;
props->video_settings_failed_ = !remote_action.vs.accepted;
if (remote_action.vs.accepted) {
props->video_settings_ = remote_action.vs;
props->applied_video_settings_ = remote_action.vs;
} else {
const auto request_id = props->video_settings_.request_id;
props->video_settings_ = props->applied_video_settings_;
props->video_settings_.request_id = request_id;
}
}
return;
}
if (remote_action.type == ControlType::video_settings) {
if (source_id != runtime->control_data_label_ ||
!runtime->IsAuthorizedController(remote_id))
return;
const auto& settings = remote_action.vs;
remote_action.vs.accepted =
UpdateVideoSettings(
runtime->peer_, remote_id.data(), remote_id.size(),
static_cast<VideoQuality>(settings.quality), settings.frame_rate,
static_cast<VideoDegradationPreference>(settings.preference)) == 0;
remote_action.type = ControlType::video_settings_status;
const auto message = remote_action.to_json();
SendReliableDataFrameToPeer(runtime->peer_, message.data(), message.size(),
runtime->control_data_label_.c_str(),
remote_id.data(), remote_id.size());
return;
}
if (remote_action.type == ControlType::privacy_status) {
if (auto props = runtime->FindRemoteSession(remote_id)) {
if (source_id != props->control_data_label_) return;
@@ -274,6 +315,25 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
if (is_client_mode) {
// Negotiate live settings only on the reliable control stream. Old
// hosts omit this capability and retain their existing video behavior.
if (props && source_id == props->control_data_label_) {
std::lock_guard lock(props->video_settings_mutex_);
const bool first_advertisement = !props->video_settings_supported_;
props->video_settings_supported_ =
remote_action.i.supports_video_settings;
if (first_advertisement && props->video_settings_supported_) {
RemoteAction settings{};
settings.type = ControlType::video_settings;
++props->video_settings_.request_id;
props->video_settings_pending_ = true;
props->video_settings_request_tick_ = SDL_GetTicks();
settings.vs = props->video_settings_;
const auto message = settings.to_json();
SendReliableDataFrame(props->peer_, message.data(), message.size(),
props->control_data_label_.c_str());
}
}
// client mode
if (props && props->remote_host_name_.empty()) {
props->remote_host_name_ = std::string(remote_action.i.host_name,
@@ -267,6 +267,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
RemoteAction remote_action = MakeHostInformation(
GetHostName(), runtime->devices_.host_display_list(),
/*supports_privacy_screen=*/true);
remote_action.i.supports_video_settings = true;
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
remote_action.i.display_list[i]);
@@ -297,6 +298,12 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
props->connection_established_ = false;
props->enable_mouse_control_ = false;
runtime->ResetRemoteServiceStatus(*props);
{
std::lock_guard lock(props->video_settings_mutex_);
props->video_settings_supported_ = false;
props->video_settings_pending_ = false;
props->video_settings_failed_ = false;
}
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
@@ -382,6 +389,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
RemoteAction remote_action = MakeHostInformation(
GetHostName(), runtime->devices_.host_display_list(),
/*supports_privacy_screen=*/true);
remote_action.i.supports_video_settings = true;
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
remote_action.i.display_list[i]);
@@ -119,6 +119,14 @@ struct RemoteSession {
bool tab_opened_ = true;
std::string remote_host_name_;
bool remote_service_status_received_ = false;
std::mutex video_settings_mutex_;
// Requested and confirmed settings for this remote session only.
VideoSettings video_settings_{2, 60, 1};
bool video_settings_supported_ = false;
VideoSettings applied_video_settings_{2, 60, 1, 0, false};
bool video_settings_pending_ = false;
bool video_settings_failed_ = false;
uint64_t video_settings_request_tick_ = 0;
std::mutex privacy_status_mutex_;
PrivacyStatus privacy_status_{};
bool privacy_status_received_ = false;
@@ -128,9 +128,6 @@ struct UserSettingsState {
char self_hosted_user_id_[17] = "";
char connection_login_identity_[17] = "";
int language_button_value_ = 0;
int video_quality_button_value_ = 2;
int video_frame_rate_button_value_ = 1;
int video_adaptation_policy_button_value_ = 1;
int video_encode_format_button_value_ = 0;
bool enable_hardware_video_codec_ = true;
bool enable_turn_ = true;
@@ -138,9 +135,6 @@ struct UserSettingsState {
char signal_server_port_[6] = "9099";
bool enable_self_hosted_ = false;
int language_button_value_last_ = 0;
int video_quality_button_value_last_ = 0;
int video_frame_rate_button_value_last_ = 0;
int video_adaptation_policy_button_value_last_ = 1;
int video_encode_format_button_value_last_ = 0;
bool enable_hardware_video_codec_last_ = false;
bool enable_turn_last_ = true;
-27
View File
@@ -38,14 +38,8 @@ export global UiStrings {
in-out property <string> delete-connection: "Delete connection";
in-out property <string> confirm-delete: "Delete this recent connection?";
in-out property <string> language: "Language";
in-out property <string> video-quality: "Video quality";
in-out property <string> frame-rate: "Frame rate";
in-out property <string> capture-method: "Capture method";
in-out property <string> capture-method-auto: "Auto";
in-out property <string> adaptation-policy: "Video preference";
in-out property <string> priority-frame-rate: "Frame rate";
in-out property <string> priority-quality: "Quality";
in-out property <string> priority-balanced: "Balanced";
in-out property <string> codec: "Video codec";
in-out property <string> hardware-codec: "Hardware video codec";
in-out property <string> turn-relay: "TURN relay";
@@ -84,9 +78,6 @@ export global UiStrings {
in-out property <string> do-not-remind: "Do not remind again";
in-out property <string> notification: "Notification";
in-out property <string> service-suppressed-message: "You will not be reminded again. You can install the service later in Settings.";
in-out property <string> quality-low: "Low";
in-out property <string> quality-medium: "Medium";
in-out property <string> quality-high: "High";
in-out property <string> codec-h264: "H.264";
in-out property <string> codec-av1: "AV1";
in-out property <string> self-hosted-settings: "Self-hosted server settings";
@@ -292,11 +283,8 @@ export component MainWindow inherits Window {
in-out property <bool> portable-service-do-not-remind: false;
in-out property <int> language-index: 0;
in-out property <int> video-quality-index: 2;
in-out property <int> frame-rate-index: 1;
in property <bool> capture-method-visible: false;
in-out property <int> capture-method-index: 0;
in-out property <int> adaptation-policy-index: 1;
in-out property <int> codec-index: 0;
in-out property <bool> hardware-codec-enabled: true;
in-out property <bool> turn-enabled: true;
@@ -1493,21 +1481,6 @@ export component MainWindow inherits Window {
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.capture-method; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.capture-method-auto, "DXGI", "WGC", "GDI"]; current-index <=> root.capture-method-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.quality-low, UiStrings.quality-medium, UiStrings.quality-high]; current-index <=> root.video-quality-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.frame-rate; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: ["30 fps", "60 fps"]; current-index <=> root.frame-rate-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.adaptation-policy; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 91px : 160px; y: 2px; width: root.compact-language ? 88px : 87px; enabled: !root.settings-session-active; model: [UiStrings.priority-frame-rate, UiStrings.priority-quality, UiStrings.priority-balanced]; current-index <=> root.adaptation-policy-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
+133 -6
View File
@@ -23,6 +23,18 @@ export struct NetworkStatsRow {
}
export global StreamStrings {
in-out property <string> video-settings: "Video settings";
in-out property <string> video-settings-unavailable: "The remote device does not support live video settings";
in-out property <string> video-quality: "Video quality";
in-out property <string> frame-rate: "Capture frame rate";
in-out property <string> adaptation-policy: "Video preference";
in-out property <string> quality-low: "Low";
in-out property <string> quality-medium: "Medium";
in-out property <string> quality-high: "High";
in-out property <string> priority-frame-rate: "Frame rate";
in-out property <string> priority-quality: "Quality";
in-out property <string> priority-balanced: "Balanced";
in-out property <string> select-display: "Select Display";
in-out property <string> send-shortcut: "Send Shortcut";
in-out property <string> privacy-enable: "Enable privacy screen";
@@ -128,6 +140,35 @@ component ControlBarButton inherits Rectangle {
}
}
component VideoSettingRow inherits VerticalLayout {
in property <string> label;
in property <[string]> options;
in property <int> selected;
in property <bool> enabled;
callback choose(int);
callback pressed;
spacing: 4px;
Text { text: root.label; color: #5c626c; font-size: 11px; height: 18px; }
HorizontalLayout {
spacing: 4px;
for option[index] in root.options: Rectangle {
height: 27px;
background: !root.enabled ? #f1f1f1 : root.selected == index ? #d7eaff
: option-touch.has-hover ? #eaf3ff : #f5f6f8;
border-radius: 4px;
border-width: 1px;
border-color: root.enabled && root.selected == index ? #4296fa : #dce0e6;
Text { text: option; color: root.enabled ? #30343b : #8b8f95;
font-size: 11px; horizontal-alignment: center; vertical-alignment: center; }
option-touch := TouchArea {
pointer-event(event) => { if event.kind == PointerEventKind.down { root.pressed(); } }
clicked => { if root.enabled { root.choose(index); } }
scroll-event(event) => { accept }
}
}
}
}
export component StreamWindow inherits Window {
title: "CrossDesk";
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
@@ -168,6 +209,12 @@ export component StreamWindow inherits Window {
in property <bool> remote-cursor-active: false;
in property <int> remote-cursor-shape: 0;
in property <bool> audio-enabled: true;
in property <bool> video-settings-enabled: false;
in property <string> video-settings-feedback: "";
in-out property <int> video-quality-index: 2;
in-out property <int> frame-rate-index: 1;
in-out property <int> adaptation-policy-index: 1;
callback change-video-settings(int, int, int);
in property <bool> fullscreen-enabled: false;
in property <bool> stats-visible: false;
in property <[NetworkStatsRow]> stats-rows;
@@ -213,16 +260,26 @@ export component StreamWindow inherits Window {
private property <bool> display-menu-animation-enabled: false;
private property <float> display-menu-progress: 0.0;
private property <bool> shortcut-menu-open: false;
in-out property <bool> video-menu-open: false;
private property <length> video-menu-width: min(320px, video.width - 8px);
private property <length> video-menu-height: root.video-settings-enabled && root.video-settings-feedback == "" ? 182px : 220px;
private property <length> video-menu-x: min(max(4px, control.x + video-button.x), video.width - root.video-menu-width - 4px);
private property <length> video-menu-y: control.y + root.control-toolbar-height + root.video-menu-height <= video.height
? control.y + root.control-toolbar-height - 1px : max(0px, control.y - root.video-menu-height);
changed selected-tab => { root.video-menu-open = false; }
changed control-expanded => { if !root.control-expanded { root.video-menu-open = false; } }
changed window-active => { if !root.window-active { root.video-menu-open = false; } }
private property <bool> control-tooltip-visible: false;
private property <ControlBarTooltip> hovered-control: {
if root.window-active && !root.control-dragging
&& !root.display-menu-open && !root.shortcut-menu-open {
&& !root.display-menu-open && !root.shortcut-menu-open && !root.video-menu-open {
if collapse-button.has-hover { return collapse-button.tooltip-info; }
if root.control-expanded {
if display-button.has-hover { return display-button.tooltip-info; }
if shortcut-button.has-hover { return shortcut-button.tooltip-info; }
if mouse-button.has-hover { return mouse-button.tooltip-info; }
if privacy-button.has-hover { return privacy-button.tooltip-info; }
if video-button.has-hover { return video-button.tooltip-info; }
if audio-button.has-hover { return audio-button.tooltip-info; }
if file-button.has-hover { return file-button.tooltip-info; }
if stats-button.has-hover { return stats-button.tooltip-info; }
@@ -305,7 +362,10 @@ export component StreamWindow inherits Window {
&& pointer-x <= shortcut-menu-x + 180px
&& pointer-y >= shortcut-menu-y
&& pointer-y <= shortcut-menu-y + 68px;
return over-control || over-display-menu || over-shortcut-menu;
let over-video-menu = root.video-menu-open
&& pointer-x >= root.video-menu-x && pointer-x <= root.video-menu-x + root.video-menu-width
&& pointer-y >= root.video-menu-y && pointer-y <= root.video-menu-y + root.video-menu-height;
return over-control || over-display-menu || over-shortcut-menu || over-video-menu;
}
begin-control-drag(press-x, press-y) => {
@@ -324,6 +384,7 @@ export component StreamWindow inherits Window {
root.display-menu-progress = 0.0;
root.display-menu-open = false;
root.shortcut-menu-open = false;
root.video-menu-open = false;
root.control-dragging = true;
}
move-control-drag(pointer-x, pointer-y) => {
@@ -697,7 +758,7 @@ export component StreamWindow inherits Window {
control := Rectangle {
x: root.control-dragging ? root.control-drag-x : root.control-docked-left ? 0px : parent.width - self.width;
y: min(max(0px, root.control-pos-y), parent.height - self.height);
width: root.control-expanded ? min(76px + 8 * root.control-button-step, parent.width) : 20px;
width: root.control-expanded ? min(76px + 9 * root.control-button-step, parent.width) : 20px;
height: root.stats-visible && root.control-expanded
? min(root.control-toolbar-height + root.stats-content-height + 8px, parent.height)
: root.control-toolbar-height;
@@ -729,6 +790,7 @@ export component StreamWindow inherits Window {
root.display-menu-progress = 0.0;
root.display-menu-open = !root.display-menu-open;
root.shortcut-menu-open = false;
root.video-menu-open = false;
if root.display-menu-open {
display-menu-open-timer.running = true;
}
@@ -744,6 +806,7 @@ export component StreamWindow inherits Window {
root.display-menu-progress = 0.0;
root.shortcut-menu-open = !root.shortcut-menu-open;
root.display-menu-open = false;
root.video-menu-open = false;
}
}
mouse-button := ControlBarButton {
@@ -766,23 +829,34 @@ export component StreamWindow inherits Window {
icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file;
clicked => { root.select-file(); }
}
stats-button := ControlBarButton {
video-button := ControlBarButton {
visible: root.control-expanded;
x: (root.control-docked-left ? 9px : 42px) + 6 * root.control-button-step; y: 7px;
icon: "\u{f1de}"; selected: root.video-menu-open;
tooltip: StreamStrings.video-settings;
clicked => {
root.video-menu-open = !root.video-menu-open;
root.display-menu-open = false;
root.shortcut-menu-open = false;
}
}
stats-button := ControlBarButton {
visible: root.control-expanded;
x: (root.control-docked-left ? 9px : 42px) + 7 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.signal; selected: root.stats-visible;
tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats;
clicked => { root.toggle-stats(); }
}
fullscreen-button := ControlBarButton {
visible: root.control-expanded;
x: (root.control-docked-left ? 9px : 42px) + 7 * root.control-button-step; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 8 * root.control-button-step; y: 7px;
icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen;
clicked => { root.toggle-fullscreen(); }
}
close-button := ControlBarButton {
visible: root.control-expanded;
x: (root.control-docked-left ? 9px : 42px) + 8 * root.control-button-step; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 9 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect;
clicked => { root.disconnect(); }
}
@@ -1010,6 +1084,59 @@ export component StreamWindow inherits Window {
}
}
// While the menu is open, outside gestures dismiss it locally.
// Keep the backdrop through pointer-up; otherwise Slint's implicit
// pointer move or release can reach the remote input surface.
if root.video-menu-open: Rectangle {
width: parent.width; height: parent.height; z: 9;
TouchArea {
pointer-event(event) => {
if event.kind == PointerEventKind.down {
root.suppress-next-remote-pointer-release = true;
}
}
clicked => { root.video-menu-open = false; }
scroll-event(event) => { accept }
}
}
if root.video-menu-open: Rectangle {
x: root.video-menu-x; y: root.video-menu-y;
width: root.video-menu-width; height: root.video-menu-height;
background: white; border-width: 1px; border-color: ImGuiLineStyle.border;
border-radius: 5px; z: 11;
TouchArea {
pointer-event(event) => { if event.kind == PointerEventKind.down { root.suppress-next-remote-pointer-release = true; } }
scroll-event(event) => { accept }
}
VerticalLayout {
padding: 10px; spacing: 8px;
VideoSettingRow {
label: StreamStrings.video-quality;
options: [StreamStrings.quality-low, StreamStrings.quality-medium, StreamStrings.quality-high];
selected: root.video-quality-index; enabled: root.video-settings-enabled;
pressed => { root.suppress-next-remote-pointer-release = true; }
choose(index) => { root.change-video-settings(index, root.frame-rate-index, root.adaptation-policy-index); }
}
VideoSettingRow {
label: StreamStrings.frame-rate; options: ["30 fps", "60 fps"];
selected: root.frame-rate-index; enabled: root.video-settings-enabled;
pressed => { root.suppress-next-remote-pointer-release = true; }
choose(index) => { root.change-video-settings(root.video-quality-index, index, root.adaptation-policy-index); }
}
VideoSettingRow {
label: StreamStrings.adaptation-policy;
options: [StreamStrings.priority-frame-rate, StreamStrings.priority-quality, StreamStrings.priority-balanced];
selected: root.adaptation-policy-index; enabled: root.video-settings-enabled;
pressed => { root.suppress-next-remote-pointer-release = true; }
choose(index) => { root.change-video-settings(root.video-quality-index, root.frame-rate-index, index); }
}
if !root.video-settings-enabled || root.video-settings-feedback != "": Text {
text: root.video-settings-enabled ? root.video-settings-feedback : StreamStrings.video-settings-unavailable; wrap: word-wrap;
color: #8b8f95; font-size: 11px;
}
}
}
if root.display-menu-open: display-menu := Rectangle {
private property <length> expanded-height: min(180px, root.displays.length * 30px + 8px);
private property <int> hovered-index:
+17 -14
View File
@@ -53,19 +53,9 @@ inline int ApplyMainWindowStrings(
strings.set_confirm_delete(
Text(localization::confirm_delete_connection[language]));
strings.set_language(Text(localization::language[language]));
strings.set_video_quality(Text(localization::video_quality[language]));
strings.set_frame_rate(Text(localization::video_frame_rate[language]));
strings.set_capture_method(Text(localization::screen_capture_method[language]));
strings.set_capture_method_auto(
Text(localization::screen_capture_method_auto[language]));
strings.set_adaptation_policy(
Text(localization::video_adaptation_policy[language]));
strings.set_priority_frame_rate(
Text(localization::video_priority_frame_rate[language]));
strings.set_priority_quality(
Text(localization::video_priority_quality[language]));
strings.set_priority_balanced(
Text(localization::video_priority_balanced[language]));
strings.set_codec(Text(localization::video_encode_format[language]));
strings.set_hardware_codec(
Text(localization::enable_hardware_video_codec[language]));
@@ -121,10 +111,6 @@ inline int ApplyMainWindowStrings(
strings.set_notification(Text(localization::notification[language]));
strings.set_service_suppressed_message(
Text(localization::windows_service_prompt_suppressed_message[language]));
strings.set_quality_low(Text(localization::video_quality_low[language]));
strings.set_quality_medium(
Text(localization::video_quality_medium[language]));
strings.set_quality_high(Text(localization::video_quality_high[language]));
strings.set_codec_h264(Text(localization::h264[language]));
strings.set_codec_av1(Text(localization::av1[language]));
strings.set_self_hosted_settings(
@@ -147,6 +133,23 @@ inline int ApplyStreamWindowStrings(
strings.set_privacy_disable(Text(localization::privacy_disable[language]));
strings.set_control_mouse(Text(localization::control_mouse[language]));
strings.set_release_mouse(Text(localization::release_mouse[language]));
strings.set_video_settings(Text(localization::video_settings[language]));
strings.set_video_settings_unavailable(
Text(localization::video_settings_unavailable[language]));
strings.set_video_quality(Text(localization::video_quality[language]));
strings.set_frame_rate(Text(localization::video_frame_rate[language]));
strings.set_adaptation_policy(
Text(localization::video_adaptation_policy[language]));
strings.set_quality_low(Text(localization::video_quality_low[language]));
strings.set_quality_medium(
Text(localization::video_quality_medium[language]));
strings.set_quality_high(Text(localization::video_quality_high[language]));
strings.set_priority_frame_rate(
Text(localization::video_priority_frame_rate[language]));
strings.set_priority_quality(
Text(localization::video_priority_quality[language]));
strings.set_priority_balanced(
Text(localization::video_priority_balanced[language]));
strings.set_audio(Text(localization::audio_capture[language]));
strings.set_mute(Text(localization::mute[language]));
strings.set_select_file(Text(localization::select_file[language]));
+18
View File
@@ -29,6 +29,8 @@ enum ControlType {
cursor_state = 8,
privacy_command = 9,
privacy_status = 10,
video_settings = 11,
video_settings_status = 12,
};
enum MouseFlag {
@@ -125,6 +127,7 @@ struct HostInfo {
int* bottom;
// Optional capability; absent in legacy desktop and web host information.
bool supports_privacy_screen;
bool supports_video_settings;
};
struct ServiceStatus {
@@ -153,6 +156,20 @@ struct PrivacyStatus {
char reason[256];
};
struct VideoSettings {
int quality; // 0: low, 1: medium, 2: high
int frame_rate; // 30 or 60
int preference; // 0: frame rate, 1: quality, 2: balanced
uint32_t request_id;
bool accepted; // Only meaningful in video_settings_status.
};
inline bool ValidVideoSettings(const VideoSettings& settings) {
return settings.quality >= 0 && settings.quality <= 2 &&
(settings.frame_rate == 30 || settings.frame_rate == 60) &&
settings.preference >= 0 && settings.preference <= 2;
}
struct RemoteAction {
ControlType type = ControlType::invalid;
union {
@@ -167,6 +184,7 @@ struct RemoteAction {
ServiceCommand c;
PrivacyCommand pc;
PrivacyStatus ps;
VideoSettings vs;
};
std::string to_json() const;
+38 -5
View File
@@ -23,6 +23,7 @@ void ResetHostInfo(HostInfo& info) {
info.right = nullptr;
info.bottom = nullptr;
info.supports_privacy_screen = false;
info.supports_video_settings = false;
}
bool AllocateHostDisplays(HostInfo& info, std::size_t count) {
@@ -134,6 +135,15 @@ std::string RemoteAction::ToJson(const RemoteAction& action) {
{"render_mode", static_cast<int>(action.cs.render_mode)},
{"hidden_reason", static_cast<int>(action.cs.hidden_reason)}};
break;
case ControlType::video_settings:
case ControlType::video_settings_status:
if (!ValidVideoSettings(action.vs)) return {};
object["video_settings"] = {{"quality", action.vs.quality},
{"frame_rate", action.vs.frame_rate},
{"preference", action.vs.preference},
{"request_id", action.vs.request_id},
{"accepted", action.vs.accepted}};
break;
case ControlType::audio_capture:
object["audio_capture"] = action.a;
break;
@@ -172,11 +182,12 @@ std::string RemoteAction::ToJson(const RemoteAction& action) {
{"right", action.i.right ? action.i.right[index] : 0},
{"bottom", action.i.bottom ? action.i.bottom[index] : 0}});
}
object["host_info"] = {{"host_name", action.i.host_name},
{"display_num", action.i.display_num},
{"displays", displays},
{"supports_privacy_screen",
action.i.supports_privacy_screen}};
object["host_info"] = {
{"host_name", action.i.host_name},
{"display_num", action.i.display_num},
{"displays", displays},
{"supports_privacy_screen", action.i.supports_privacy_screen},
{"supports_video_settings", action.i.supports_video_settings}};
break;
}
case ControlType::invalid:
@@ -277,6 +288,26 @@ bool RemoteAction::FromJson(const std::string& json_string,
std::clamp(output.cs.visual_offset_y, -1.0f, 1.0f);
break;
}
case ControlType::video_settings:
case ControlType::video_settings_status: {
const auto& settings = object.at("video_settings");
for (const auto* field : {"quality", "frame_rate", "preference"}) {
if (!settings.at(field).is_number_integer()) return false;
const auto value = settings.at(field).get<int64_t>();
if (value < 0 || value > 60) return false;
}
output.vs = {settings.at("quality").get<int>(),
settings.at("frame_rate").get<int>(),
settings.at("preference").get<int>()};
if (!ValidVideoSettings(output.vs)) return false;
const auto& request_id = settings.at("request_id");
if (!request_id.is_number_integer() || request_id.get<int64_t>() < 0 ||
request_id.get<uint64_t>() > UINT32_MAX)
return false;
output.vs.request_id = request_id.get<uint32_t>();
output.vs.accepted = settings.value("accepted", false);
break;
}
case ControlType::audio_capture:
output.a = object.at("audio_capture").get<bool>();
break;
@@ -334,6 +365,8 @@ bool RemoteAction::FromJson(const std::string& json_string,
output.i.supports_privacy_screen =
privacy_support != host_info_object.end() &&
privacy_support->is_boolean() && privacy_support->get<bool>();
output.i.supports_video_settings =
host_info_object.value("supports_video_settings", false);
const std::string host_name =
host_info_object.at("host_name").get<std::string>();
std::strncpy(output.i.host_name, host_name.c_str(),