diff --git a/apps/desktop/src/config_center/config_center.cpp b/apps/desktop/src/config_center/config_center.cpp index 9cb3365c..4361a566 100644 --- a/apps/desktop/src/config_center/config_center.cpp +++ b/apps/desktop/src/config_center/config_center.cpp @@ -12,12 +12,6 @@ bool IsValidTurnModeValue(long value) { value <= static_cast(ConfigCenter::TURN_MODE::FORCE_TCP); } -bool IsValidVideoAdaptationPolicyValue(long value) { - using Policy = ConfigCenter::VIDEO_ADAPTATION_POLICY; - return value >= static_cast(Policy::FRAME_RATE_PRIORITY) && - value <= static_cast(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_value); } - video_quality_ = static_cast(ini_.GetLongValue( - section_, "video_quality", static_cast(video_quality_))); - - video_frame_rate_ = static_cast(ini_.GetLongValue( - section_, "video_frame_rate", static_cast(video_frame_rate_))); - const long screen_capture_method_value = ini_.GetLongValue( section_, "screen_capture_method", static_cast(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(video_adaptation_policy_)); - if (IsValidVideoAdaptationPolicyValue(video_adaptation_policy_value)) { - video_adaptation_policy_ = - static_cast(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( ini_.GetLongValue(section_, "video_encode_format", static_cast(video_encode_format_))); @@ -156,14 +137,8 @@ int ConfigCenter::Load() { int ConfigCenter::Save() { ini_.SetLongValue(section_, "language", static_cast(language_)); - ini_.SetLongValue(section_, "video_quality", - static_cast(video_quality_)); - ini_.SetLongValue(section_, "video_frame_rate", - static_cast(video_frame_rate_)); ini_.SetLongValue(section_, "screen_capture_method", static_cast(screen_capture_method_)); - ini_.SetLongValue(section_, "video_adaptation_policy", - static_cast(video_adaptation_policy_)); ini_.SetLongValue(section_, "video_encode_format", static_cast(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(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(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(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(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_; } diff --git a/apps/desktop/src/config_center/config_center.h b/apps/desktop/src/config_center/config_center.h index bf4ca77d..4350c86d 100644 --- a/apps/desktop/src/config_center/config_center.h +++ b/apps/desktop/src/config_center/config_center.h @@ -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; diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index 5e6082df..f29b29de 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -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(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(video_quality_button_value_)); - config_center_->SetVideoFrameRate(static_cast( - video_frame_rate_button_value_)); - config_center_->SetVideoAdaptationPolicy( - static_cast( - video_adaptation_policy_button_value_)); + privacy_.SetText( + {localization::privacy_screen_unlock_hint[localization_language_index_]}); config_center_->SetVideoEncodeFormat( static_cast( 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_; diff --git a/apps/desktop/src/gui/assets/localization/localization_data.h b/apps/desktop/src/gui/assets/localization/localization_data.h index 7dcb8b7a..52e12d8b 100644 --- a/apps/desktop/src/gui/assets/localization/localization_data.h +++ b/apps/desktop/src/gui/assets/localization/localization_data.h @@ -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"Частота захвата видео:") \ diff --git a/apps/desktop/src/gui/features/devices/session_device_manager.cpp b/apps/desktop/src/gui/features/devices/session_device_manager.cpp index e3d4f68c..50a60560 100644 --- a/apps/desktop/src/gui/features/devices/session_device_manager.cpp +++ b/apps/desktop/src/gui/features/devices/session_device_manager.cpp @@ -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; diff --git a/apps/desktop/src/gui/features/settings/settings_manager.cpp b/apps/desktop/src/gui/features/settings/settings_manager.cpp index 7c185eb9..100446dc 100644 --- a/apps/desktop/src/gui/features/settings/settings_manager.cpp +++ b/apps/desktop/src/gui/features/settings/settings_manager.cpp @@ -297,12 +297,6 @@ int SettingsManager::Load() { owner_.language_button_value_ = localization::detail::ClampLanguageIndex( static_cast(owner_.config_center_->GetLanguage())); - owner_.video_quality_button_value_ = - static_cast(owner_.config_center_->GetVideoQuality()); - owner_.video_frame_rate_button_value_ = - static_cast(owner_.config_center_->GetVideoFrameRate()); - owner_.video_adaptation_policy_button_value_ = - static_cast(owner_.config_center_->GetVideoAdaptationPolicy()); owner_.video_encode_format_button_value_ = static_cast(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_ = diff --git a/apps/desktop/src/gui/features/settings/settings_manager.h b/apps/desktop/src/gui/features/settings/settings_manager.h index 8831bd94..2a6a4ab9 100644 --- a/apps/desktop/src/gui/features/settings/settings_manager.h +++ b/apps/desktop/src/gui/features/settings/settings_manager.h @@ -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; diff --git a/apps/desktop/src/gui/runtime/gui_runtime.cpp b/apps/desktop/src/gui/runtime/gui_runtime.cpp index 7d75e009..3463b594 100644 --- a/apps/desktop/src/gui/runtime/gui_runtime.cpp +++ b/apps/desktop/src/gui/runtime/gui_runtime.cpp @@ -136,28 +136,12 @@ int GuiRuntime::CreateConnectionPeer() { params_.turn_mode = static_cast(config_center_->GetTurnMode()); params_.enable_srtp = config_center_->IsEnableSrtp(); params_.video_content_type = VideoContentType::ScreenContent; - params_.video_quality = - static_cast(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; diff --git a/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp b/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp index 453c36d7..c2d18c5a 100644 --- a/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp +++ b/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp @@ -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(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(settings.quality), settings.frame_rate, + static_cast(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, diff --git a/apps/desktop/src/gui/runtime/peer_event_handler.cpp b/apps/desktop/src/gui/runtime/peer_event_handler.cpp index 563ae74f..f19f7b41 100644 --- a/apps/desktop/src/gui/runtime/peer_event_handler.cpp +++ b/apps/desktop/src/gui/runtime/peer_event_handler.cpp @@ -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]); diff --git a/apps/desktop/src/gui/runtime/remote_session.h b/apps/desktop/src/gui/runtime/remote_session.h index a30edde5..3b8fdc5e 100644 --- a/apps/desktop/src/gui/runtime/remote_session.h +++ b/apps/desktop/src/gui/runtime/remote_session.h @@ -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; diff --git a/apps/desktop/src/gui/runtime/runtime_state.h b/apps/desktop/src/gui/runtime/runtime_state.h index 2fff8f17..47b03cbb 100644 --- a/apps/desktop/src/gui/runtime/runtime_state.h +++ b/apps/desktop/src/gui/runtime/runtime_state.h @@ -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; diff --git a/apps/desktop/src/gui/ui/main_window.slint b/apps/desktop/src/gui/ui/main_window.slint index 7671662c..89fc0688 100644 --- a/apps/desktop/src/gui/ui/main_window.slint +++ b/apps/desktop/src/gui/ui/main_window.slint @@ -38,14 +38,8 @@ export global UiStrings { in-out property delete-connection: "Delete connection"; in-out property confirm-delete: "Delete this recent connection?"; in-out property language: "Language"; - in-out property video-quality: "Video quality"; - in-out property frame-rate: "Frame rate"; in-out property capture-method: "Capture method"; in-out property capture-method-auto: "Auto"; - in-out property adaptation-policy: "Video preference"; - in-out property priority-frame-rate: "Frame rate"; - in-out property priority-quality: "Quality"; - in-out property priority-balanced: "Balanced"; in-out property codec: "Video codec"; in-out property hardware-codec: "Hardware video codec"; in-out property turn-relay: "TURN relay"; @@ -84,9 +78,6 @@ export global UiStrings { in-out property do-not-remind: "Do not remind again"; in-out property notification: "Notification"; in-out property service-suppressed-message: "You will not be reminded again. You can install the service later in Settings."; - in-out property quality-low: "Low"; - in-out property quality-medium: "Medium"; - in-out property quality-high: "High"; in-out property codec-h264: "H.264"; in-out property codec-av1: "AV1"; in-out property self-hosted-settings: "Self-hosted server settings"; @@ -292,11 +283,8 @@ export component MainWindow inherits Window { in-out property portable-service-do-not-remind: false; in-out property language-index: 0; - in-out property video-quality-index: 2; - in-out property frame-rate-index: 1; in property capture-method-visible: false; in-out property capture-method-index: 0; - in-out property adaptation-policy-index: 1; in-out property codec-index: 0; in-out property hardware-codec-enabled: true; in-out property 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; } diff --git a/apps/desktop/src/gui/ui/stream_window.slint b/apps/desktop/src/gui/ui/stream_window.slint index 2a5acedc..f0537627 100644 --- a/apps/desktop/src/gui/ui/stream_window.slint +++ b/apps/desktop/src/gui/ui/stream_window.slint @@ -23,6 +23,18 @@ export struct NetworkStatsRow { } export global StreamStrings { + in-out property video-settings: "Video settings"; + in-out property video-settings-unavailable: "The remote device does not support live video settings"; + in-out property video-quality: "Video quality"; + in-out property frame-rate: "Capture frame rate"; + in-out property adaptation-policy: "Video preference"; + in-out property quality-low: "Low"; + in-out property quality-medium: "Medium"; + in-out property quality-high: "High"; + in-out property priority-frame-rate: "Frame rate"; + in-out property priority-quality: "Quality"; + in-out property priority-balanced: "Balanced"; + in-out property select-display: "Select Display"; in-out property send-shortcut: "Send Shortcut"; in-out property privacy-enable: "Enable privacy screen"; @@ -128,6 +140,35 @@ component ControlBarButton inherits Rectangle { } } +component VideoSettingRow inherits VerticalLayout { + in property label; + in property <[string]> options; + in property selected; + in property 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 remote-cursor-active: false; in property remote-cursor-shape: 0; in property audio-enabled: true; + in property video-settings-enabled: false; + in property video-settings-feedback: ""; + in-out property video-quality-index: 2; + in-out property frame-rate-index: 1; + in-out property adaptation-policy-index: 1; + callback change-video-settings(int, int, int); in property fullscreen-enabled: false; in property stats-visible: false; in property <[NetworkStatsRow]> stats-rows; @@ -213,16 +260,26 @@ export component StreamWindow inherits Window { private property display-menu-animation-enabled: false; private property display-menu-progress: 0.0; private property shortcut-menu-open: false; + in-out property video-menu-open: false; + private property video-menu-width: min(320px, video.width - 8px); + private property video-menu-height: root.video-settings-enabled && root.video-settings-feedback == "" ? 182px : 220px; + private property video-menu-x: min(max(4px, control.x + video-button.x), video.width - root.video-menu-width - 4px); + private property 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 control-tooltip-visible: false; private property 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 expanded-height: min(180px, root.displays.length * 30px + 8px); private property hovered-index: diff --git a/apps/desktop/src/gui/ui/ui_localization.h b/apps/desktop/src/gui/ui/ui_localization.h index d00e0f12..5401e96c 100644 --- a/apps/desktop/src/gui/ui/ui_localization.h +++ b/apps/desktop/src/gui/ui/ui_localization.h @@ -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])); diff --git a/deps/submodules/minirtc b/deps/submodules/minirtc index a25a3b4c..000ab788 160000 --- a/deps/submodules/minirtc +++ b/deps/submodules/minirtc @@ -1 +1 @@ -Subproject commit a25a3b4c4abb041ad129d60abc8b2508aa7c33fa +Subproject commit 000ab78890c624a52aff4c1894ca3f633460b10f diff --git a/libs/wire/include/remote_action.h b/libs/wire/include/remote_action.h index cf306049..12b111a2 100644 --- a/libs/wire/include/remote_action.h +++ b/libs/wire/include/remote_action.h @@ -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; diff --git a/libs/wire/src/remote_action.cpp b/libs/wire/src/remote_action.cpp index e1417079..77b37db1 100644 --- a/libs/wire/src/remote_action.cpp +++ b/libs/wire/src/remote_action.cpp @@ -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(action.cs.render_mode)}, {"hidden_reason", static_cast(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(); + if (value < 0 || value > 60) return false; + } + output.vs = {settings.at("quality").get(), + settings.at("frame_rate").get(), + settings.at("preference").get()}; + if (!ValidVideoSettings(output.vs)) return false; + const auto& request_id = settings.at("request_id"); + if (!request_id.is_number_integer() || request_id.get() < 0 || + request_id.get() > UINT32_MAX) + return false; + output.vs.request_id = request_id.get(); + output.vs.accepted = settings.value("accepted", false); + break; + } case ControlType::audio_capture: output.a = object.at("audio_capture").get(); 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(); + 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::strncpy(output.i.host_name, host_name.c_str(),