#include "runtime/gui_runtime.h" #include #include #include #include #include #include #include #include #include #include #include #include "localization.h" #include "platform/video_renderer.h" #include "rd_log.h" #ifdef __linux__ #include "platform/linux/headless/headless_console.h" #endif namespace crossdesk { GuiRuntime::GuiRuntime() : clipboard_(*this), devices_(*this), transfers_(*this), settings_(*this), keyboard_(*this) {} GuiRuntime::~GuiRuntime() { WaitForSessionCleanup(); } void GuiRuntime::SendRemoteInput(const RemoteAction& action) { const bool mouse = action.type == ControlType::mouse; if (!mouse && action.type != ControlType::keyboard && action.type != ControlType::keyboard_state) return; const std::string& target_id = controlled_remote_id_.empty() ? focused_remote_id_ : controlled_remote_id_; if (target_id.empty()) return; const bool group = gallery_group_control_id_ == target_id; const bool releasing = mouse ? action.m.flag == MouseFlag::left_up || action.m.flag == MouseFlag::right_up || action.m.flag == MouseFlag::middle_up : action.type == ControlType::keyboard ? action.k.flag == KeyFlag::key_up : action.ks.pressed_count == 0; const auto available = [](const auto& props) { return props && props->peer_ && !props->closing_ && props->connection_status_ == ConnectionStatus::Connected; }; std::shared_lock lock(remote_sessions_mutex_); const auto source = remote_sessions_.find(target_id); // A lost leader must stop new input, but its followers still need key/button // releases before group control is cleared by the UI. if (!releasing && (source == remote_sessions_.end() || !available(source->second))) return; const auto message = action.to_json(); for (const auto& [id, props] : remote_sessions_) { if ((!group && id != target_id) || !available(props)) continue; if (mouse) { std::lock_guard cursor_lock(props->remote_cursor_state_mutex_); props->cursor_presentation_.NoteInput(SDL_GetTicks()); } // Reuse exactly the leader's mapped coordinates and key metadata. Neither // gallery filters nor the follower's preview dimensions affect input. const int result = SendReliableDataFrame( props->peer_, message.data(), message.size(), mouse ? props->mouse_label_.c_str() : props->keyboard_label_.c_str()); if (result != 0) LOG_WARN("Send remote input failed, remote_id={}, ret={}", id, result); } } int GuiRuntime::CreateConnectionPeer() { // Tick retries after the old peer's leave notification and socket shutdown. if (connection_peer_cleanup_.valid()) return 0; { std::unique_lock lock(remote_sessions_mutex_); peer_events_ = std::make_shared(*this); } params_.use_cfg_file = false; std::string signal_server_ip; int signal_server_port; if (config_center_->IsSelfHosted()) { signal_server_ip = config_center_->GetSignalServerHost(); signal_server_port = config_center_->GetSignalServerPort(); std::string current_self_hosted_ip = config_center_->GetSignalServerHost(); const bool use_cached_id = settings_.LoadCachedSelfHostedIdentity(); if (!use_cached_id) { LOG_INFO( "secure_cache_v2.enc not found, will use empty id to get new id from " "server"); } if (use_cached_id && strlen(self_hosted_id_) > 0) { memset(&self_hosted_user_id_, 0, sizeof(self_hosted_user_id_)); strncpy(self_hosted_user_id_, self_hosted_id_, sizeof(self_hosted_user_id_) - 1); self_hosted_user_id_[sizeof(self_hosted_user_id_) - 1] = '\0'; params_.user_id = self_hosted_user_id_; } else { memset(&self_hosted_user_id_, 0, sizeof(self_hosted_user_id_)); params_.user_id = self_hosted_user_id_; LOG_INFO( "Using empty id for self-hosted server, server will assign new id"); } } else { signal_server_ip = config_center_->GetDefaultServerHost(); signal_server_port = config_center_->GetDefaultSignalServerPort(); settings_.ActivateCachedPublicIdentity(); params_.user_id = client_id_with_password_; } const bool self_hosted = config_center_->IsSelfHosted(); const std::string pending_identity = settings_.PendingPasswordChangeIdentity( self_hosted, signal_server_ip, signal_server_port); bool try_pending_identity = false; { std::lock_guard lock(password_change_mutex_); if (pending_identity.empty()) { credential_recovery_in_progress_ = false; credential_recovery_attempt_pending_ = false; credential_recovery_retry_active_ = false; credential_recovery_promote_pending_ = false; credential_recovery_clear_pending_ = false; } else if (!credential_recovery_in_progress_) { credential_recovery_in_progress_ = true; credential_recovery_attempt_pending_ = true; LOG_INFO("Recovering an interrupted password change for [{}]", client_id_); } try_pending_identity = credential_recovery_in_progress_ && credential_recovery_attempt_pending_ && !pending_identity.empty(); } const char *active_identity = self_hosted ? self_hosted_user_id_ : client_id_with_password_; #ifdef __linux__ // Publish the committed identity; an unconfirmed recovery credential must // not be advertised as the current connection password. HeadlessConsole::Instance().SetIdentity(active_identity); #endif const std::string login_identity = try_pending_identity ? pending_identity : std::string(active_identity); std::memset(connection_login_identity_, 0, sizeof(connection_login_identity_)); std::strncpy(connection_login_identity_, login_identity.c_str(), sizeof(connection_login_identity_) - 1); params_.user_id = connection_login_identity_; // self hosted server config strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(), sizeof(signal_server_ip_self_) - 1); signal_server_ip_self_[sizeof(signal_server_ip_self_) - 1] = '\0'; int signal_port = config_center_->GetSignalServerPort(); if (signal_port > 0) { strncpy(signal_server_port_self_, std::to_string(signal_port).c_str(), sizeof(signal_server_port_self_) - 1); signal_server_port_self_[sizeof(signal_server_port_self_) - 1] = '\0'; } else { signal_server_port_self_[0] = '\0'; } // peer config strncpy((char *)params_.signal_server_ip, signal_server_ip.c_str(), sizeof(params_.signal_server_ip) - 1); params_.signal_server_ip[sizeof(params_.signal_server_ip) - 1] = '\0'; params_.signal_server_port = signal_server_port; strncpy(params_.log_path, dll_log_path_.c_str(), sizeof(params_.log_path) - 1); params_.log_path[sizeof(params_.log_path) - 1] = '\0'; params_.hardware_acceleration = config_center_->IsHardwareVideoCodec(); #if defined(_WIN32) || defined(__linux__) || defined(__APPLE__) // Windows and Linux retain pooled CPU NV12 frames or CUDA device frames; // macOS retains VideoToolbox CVPixelBuffers for direct Metal sampling. All // platforms fall back to a packed CPU copy when native upload is unavailable. params_.native_video_output = true; #else params_.native_video_output = false; #endif params_.av1_encoding = config_center_->GetVideoEncodeFormat() == ConfigCenter::VIDEO_ENCODE_FORMAT::AV1 ? true : false; params_.turn_mode = static_cast(config_center_->GetTurnMode()); params_.enable_srtp = config_center_->IsEnableSrtp(); params_.video_content_type = VideoContentType::ScreenContent; // 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; params_.on_receive_video_frame = PeerEventHandler::OnReceiveVideoBuffer; params_.on_signal_status = PeerEventHandler::OnSignalStatus; params_.on_signal_message = PeerEventHandler::OnSignalMessage; params_.on_connection_status = PeerEventHandler::OnConnectionStatus; params_.on_net_status_report = PeerEventHandler::OnNetStatusReport; params_.user_data = peer_events_.get(); // The previous peer may have left a terminal status behind. Reset it before // Init() starts emitting callbacks for the newly selected server. signal_connected_ = false; device_presence_cache_.SetSignalConnected(false); need_to_send_recent_connections_ = true; signal_status_ = SignalStatus::SignalConnecting; { std::unique_lock lock(remote_sessions_mutex_); peer_ = CreatePeer(¶ms_); } if (peer_) { LOG_INFO("Create peer instance [{}] successful", client_id_); Init(peer_); LOG_INFO("Peer [{}] init finish", client_id_); } else { LOG_INFO("Create peer [{}] instance failed", client_id_); } if (0 == devices_.InitializeScreenCapturer()) { const auto &displays = devices_.display_info_list(); for (size_t index = 0; index < displays.size(); ++index) { const std::string stream_id = MakeDisplayStreamId(index); AddVideoStream(peer_, stream_id.c_str()); } AddAudioStream(peer_, audio_label_.c_str()); AddDataStream(peer_, data_label_.c_str(), false); AddDataStream(peer_, mouse_label_.c_str(), true); AddDataStream(peer_, keyboard_label_.c_str(), true); AddDataStream(peer_, control_data_label_.c_str(), true); AddDataStream(peer_, file_label_.c_str(), true); AddDataStream(peer_, file_feedback_label_.c_str(), true); AddDataStream(peer_, clipboard_label_.c_str(), true); return 0; } else { return -1; } } void GuiRuntime::HandleRecentConnections() { if (thumbnail_ && session_cleanup_tasks_.empty()) { std::vector> sessions; { std::shared_lock lock(remote_sessions_mutex_); for (const auto& [_, props] : remote_sessions_) if (props && !props->closing_ && props->was_connected_ && props->recent_connection_dirty_.exchange(false)) sessions.push_back(props); } for (const auto& props : sessions) { std::string host_name; HostPlatform platform; { std::lock_guard lock(props->remote_version_mutex_); host_name = props->remote_host_name_; platform = props->recent_platform_; } // Record a successful connection before a preview or host info arrives. // The close path still updates its final thumbnail. Remember password // controls only credential storage, not whether this device is listed. if (thumbnail_->SaveToThumbnail( nullptr, 0, 0, props->remote_id_, host_name, props->remember_password_ ? props->remote_password_ : "", platform) == 0) reload_recent_connections_ = true; else LOG_ERROR("[{}] Failed to save recent connection", props->remote_id_); } } if (session_cleanup_tasks_.empty() && reload_recent_connections_ && thumbnail_) { uint32_t now_time = SDL_GetTicks(); if (now_time - recent_connection_image_save_time_ >= 50) { int ret = thumbnail_->LoadThumbnail(recent_connections_, &recent_connection_image_width_, &recent_connection_image_height_); if (!ret) { LOG_INFO("Load recent connection thumbnails"); } reload_recent_connections_ = false; recent_connection_ids_.clear(); for (const auto &conn : recent_connections_) { recent_connection_ids_.push_back(conn.first); } need_to_send_recent_connections_ = true; } } } void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) { GuiRuntime *runtime = static_cast(userdata); if (!runtime) { return; } if (1) { std::shared_lock lock(runtime->remote_sessions_mutex_); uint64_t captured_timestamp = 0; for (const auto &it : runtime->remote_sessions_) { auto props = it.second; if (props->connection_status_.load() == ConnectionStatus::Connected) { if (props->peer_) { if (captured_timestamp == 0) { const int64_t timestamp_us = GetSystemTimeMicros(props->peer_); if (timestamp_us > 0) { captured_timestamp = static_cast(timestamp_us); } } MiniRtcAudioFrame frame{}; frame.data = reinterpret_cast(stream); frame.size = static_cast(len); frame.captured_timestamp = captured_timestamp; SendAudioFrame(props->peer_, &frame, runtime->audio_label_.c_str()); } } } } else { memcpy(runtime->audio_buffer_, stream, len); runtime->audio_len_ = len; SDL_Delay(10); runtime->audio_buffer_fresh_ = true; } } void GuiRuntime::SdlCaptureAudioOut([[maybe_unused]] void *userdata, [[maybe_unused]] Uint8 *stream, [[maybe_unused]] int len) { } } // namespace crossdesk