[refactor] reorganize platform and wire sources

This commit is contained in:
dijunkun
2026-09-01 01:29:30 +08:00
parent fb0ab4f14e
commit 90184f5251
354 changed files with 2837 additions and 2456 deletions
@@ -0,0 +1,477 @@
#include <algorithm>
#include <chrono>
#include <cstring>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include <vector>
#include <display_stream_id.h>
#include "localization.h"
#include "platform.h"
#include "platform/video_renderer.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
namespace {
constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5);
} // namespace
void GuiRuntime::HandleConnectionStatusChange() {
if (signal_connected_ && peer_ && need_to_send_recent_connections_) {
if (!recent_connection_ids_.empty()) {
nlohmann::json j;
j["type"] = "recent_connections_presence";
j["user_id"] = client_id_;
j["devices"] = nlohmann::json::array();
for (const auto& id : recent_connection_ids_) {
std::string pure_id = id;
size_t pos_y = pure_id.find('Y');
size_t pos_n = pure_id.find('N');
size_t pos = std::string::npos;
if (pos_y != std::string::npos &&
(pos_n == std::string::npos || pos_y < pos_n)) {
pos = pos_y;
} else if (pos_n != std::string::npos) {
pos = pos_n;
}
if (pos != std::string::npos) {
pure_id = pure_id.substr(0, pos);
}
j["devices"].push_back(pure_id);
}
auto s = j.dump();
SendSignalMessage(peer_, s.data(), s.size());
}
}
need_to_send_recent_connections_ = false;
}
void GuiRuntime::HandlePendingPresenceProbe() {
bool has_action = false;
bool should_connect = false;
bool remember_password = false;
std::string remote_id;
std::string password;
{
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
if (!pending_presence_probe_ || !pending_presence_result_ready_) {
return;
}
has_action = true;
should_connect = pending_presence_online_;
remote_id = pending_presence_remote_id_;
password = pending_presence_password_;
remember_password = pending_presence_remember_password_;
pending_presence_probe_ = false;
pending_presence_result_ready_ = false;
pending_presence_online_ = false;
pending_presence_remote_id_.clear();
pending_presence_password_.clear();
pending_presence_remember_password_ = false;
}
if (!has_action) {
return;
}
if (should_connect) {
ConnectTo(remote_id, password.c_str(), remember_password, true);
return;
}
offline_warning_text_ =
localization::device_offline[localization_language_index_];
show_offline_warning_window_ = true;
}
void GuiRuntime::HandlePresenceProbeTimeout() {
const auto now = std::chrono::steady_clock::now();
bool presence_probe_timed_out = false;
std::string presence_remote_id;
{
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
if (pending_presence_probe_ && !pending_presence_result_ready_ &&
now - pending_presence_probe_started_at_ >= kPresenceProbeTimeout) {
presence_probe_timed_out = true;
presence_remote_id = pending_presence_remote_id_;
pending_presence_probe_ = false;
pending_presence_result_ready_ = false;
pending_presence_online_ = false;
pending_presence_remote_id_.clear();
pending_presence_password_.clear();
pending_presence_remember_password_ = false;
}
}
if (presence_probe_timed_out) {
offline_warning_text_ =
localization::device_offline[localization_language_index_];
show_offline_warning_window_ = true;
LOG_WARN("Presence probe timed out for [{}]", presence_remote_id);
}
}
void GuiRuntime::HandleServerControllerDisconnected(
const std::string& remote_id, const char* reason) {
keyboard_.ReleaseRemotePressedKeys(remote_id, reason);
{
std::lock_guard lock(remote_pointer_input_mutex_);
last_remote_pointer_input_time_.erase(remote_id);
}
bool has_connected_controller = false;
bool has_web_controller = false;
std::string remaining_controller_id;
{
std::unique_lock lock(connection_status_mutex_);
connection_status_.erase(remote_id);
connection_host_names_.erase(remote_id);
for (const auto& [id, status] : connection_status_) {
if (status != ConnectionStatus::Connected) {
continue;
}
has_connected_controller = true;
has_web_controller =
has_web_controller || id.find("web") != std::string::npos;
if (remaining_controller_id.empty()) {
remaining_controller_id = id;
}
}
}
show_cursor_ = has_web_controller;
if (has_connected_controller) {
remote_client_id_ = remaining_controller_id;
return;
}
need_to_create_server_window_.store(false, std::memory_order_release);
need_to_destroy_server_window_.store(true, std::memory_order_release);
is_server_mode_ = false;
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
// Keep Wayland capture session warm to avoid black screen on subsequent
// reconnects.
start_screen_capturer_ = true;
LOG_INFO(
"Keeping Wayland screen capturer running after disconnect to "
"preserve reconnect stability");
} else {
start_screen_capturer_ = false;
}
#else
start_screen_capturer_ = false;
#endif
start_speaker_capturer_ = false;
start_mouse_controller_ = false;
start_keyboard_capturer_ = false;
remote_client_id_.clear();
if (audio_capture_) {
devices_.StopSpeakerCapturer();
audio_capture_ = false;
}
devices_.ResetToInitialDisplay();
}
int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
const char* password,
bool remember_password) {
if (!signal_connected_ || !peer_) {
return -1;
}
{
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
pending_presence_probe_ = true;
pending_presence_result_ready_ = false;
pending_presence_online_ = false;
pending_presence_probe_started_at_ = std::chrono::steady_clock::now();
pending_presence_remote_id_ = remote_id;
pending_presence_password_ = password ? password : "";
pending_presence_remember_password_ = remember_password;
}
nlohmann::json j;
j["type"] = "recent_connections_presence";
j["user_id"] = client_id_;
j["devices"] = nlohmann::json::array({remote_id});
auto s = j.dump();
int ret = SendSignalMessage(peer_, s.data(), s.size());
if (ret != 0) {
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
pending_presence_probe_ = false;
pending_presence_result_ready_ = false;
pending_presence_online_ = false;
pending_presence_remote_id_.clear();
pending_presence_password_.clear();
pending_presence_remember_password_ = false;
}
return ret;
}
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
int video_width = 0;
int video_height = 0;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
frame_snapshot = props->front_frame_;
video_width = props->video_width_;
video_height = props->video_height_;
if ((!frame_snapshot || frame_snapshot->empty()) &&
props->thumbnail_frame_ && !props->thumbnail_frame_->empty()) {
frame_snapshot = props->thumbnail_frame_;
video_width = props->thumbnail_width_;
video_height = props->thumbnail_height_;
}
}
auto* native_renderer = video_renderer_.get();
if ((!frame_snapshot || frame_snapshot->empty()) && native_renderer) {
auto native_snapshot = std::make_shared<std::vector<unsigned char>>();
if (native_renderer->CopyLatestNv12(props->remote_id_,
native_snapshot.get(), &video_width,
&video_height)) {
frame_snapshot = std::move(native_snapshot);
}
}
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
video_height > 0) {
std::vector<unsigned char> buffer_copy(*frame_snapshot);
std::string remote_id = props->remote_id_;
std::string remote_host_name = props->remote_host_name_;
std::string password =
props->remember_password_ ? props->remote_password_ : "";
std::thread save_thread([buffer_copy, video_width, video_height, remote_id,
remote_host_name, password,
thumbnail = thumbnail_]() {
thumbnail->SaveToThumbnail((char*)buffer_copy.data(), video_width,
video_height, remote_id, remote_host_name,
password);
});
{
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
thumbnail_save_threads_.emplace_back(std::move(save_thread));
}
}
if (native_renderer) {
native_renderer->DiscardStream(props->remote_id_);
video_frame_dirty_.store(true, std::memory_order_release);
}
if (props->peer_) {
LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_);
LeaveConnection(props->peer_, props->remote_id_.c_str());
LOG_INFO("Destroy peer [{}]", props->local_id_);
DestroyPeer(&props->peer_);
}
}
void GuiRuntime::CloseAllRemoteSessions() {
if (peer_) {
LOG_INFO("[{}] Leave connection [{}]", client_id_, client_id_);
LeaveConnection(peer_, client_id_);
is_client_mode_ = false;
devices_.StopMouseController();
devices_.StopScreenCapturer();
devices_.StopSpeakerCapturer();
devices_.StopKeyboardCapturer();
LOG_INFO("Destroy peer [{}]", client_id_);
DestroyPeer(&peer_);
}
{
// std::shared_lock lock(remote_sessions_mutex_);
for (auto& it : remote_sessions_) {
auto props = it.second;
CloseRemoteSession(props);
}
}
{
// std::unique_lock lock(remote_sessions_mutex_);
remote_sessions_.clear();
}
}
void GuiRuntime::WaitForThumbnailSaveTasks() {
std::vector<std::thread> threads_to_join;
{
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
threads_to_join.swap(thumbnail_save_threads_);
}
if (threads_to_join.empty()) {
return;
}
for (auto& thread : threads_to_join) {
if (thread.joinable()) {
thread.join();
}
}
}
void GuiRuntime::ResetRemoteSessionResources(
std::shared_ptr<RemoteSession> props) {
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->front_frame_.reset();
props->back_frame_.reset();
props->thumbnail_frame_.reset();
props->thumbnail_width_ = 0;
props->thumbnail_height_ = 0;
props->background_snapshot_time_ = {};
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = false;
}
{
std::lock_guard<std::mutex> lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
}
}
std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
const std::string& remote_id) {
if (remote_id.empty()) {
return nullptr;
}
std::shared_lock lock(remote_sessions_mutex_);
auto it = remote_sessions_.find(remote_id);
if (it == remote_sessions_.end()) {
return nullptr;
}
return it->second;
}
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
bool remember_password, bool bypass_presence_check) {
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
int ret =
RequestSingleDevicePresence(remote_id, password, remember_password);
if (ret != 0) {
offline_warning_text_ =
localization::device_offline[localization_language_index_];
show_offline_warning_window_ = true;
LOG_WARN("Presence probe failed for [{}], ret={}", remote_id, ret);
} else {
LOG_INFO("Presence probe requested for [{}] before connect", remote_id);
}
return -1;
}
LOG_INFO("Connect to [{}]", remote_id);
focused_remote_id_ = remote_id;
// std::shared_lock shared_lock(remote_sessions_mutex_);
bool exists = (remote_sessions_.find(remote_id) != remote_sessions_.end());
// shared_lock.unlock();
if (!exists) {
PeerPtr* peer_to_init = nullptr;
std::string local_id;
{
// std::unique_lock unique_lock(remote_sessions_mutex_);
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
remote_sessions_[remote_id] = std::make_shared<RemoteSession>();
auto props = remote_sessions_[remote_id];
props->local_id_ = "C-" + std::string(client_id_);
props->remote_id_ = remote_id;
memcpy(&props->params_, &params_, sizeof(Params));
props->params_.user_id = props->local_id_.c_str();
props->peer_ = CreatePeer(&props->params_);
props->control_window_width_ = title_bar_height_ * 10.0f;
props->control_window_height_ = title_bar_height_ * 1.3f;
props->control_window_min_width_ = title_bar_height_ * 0.65f;
props->control_window_min_height_ = title_bar_height_ * 1.3f;
props->control_window_max_width_ = title_bar_height_ * 10.0f;
props->control_window_max_height_ = title_bar_height_ * 7.0f;
props->connection_status_.store(ConnectionStatus::Connecting);
show_connection_status_window_ = true;
if (!props->peer_) {
LOG_INFO("Create peer [{}] instance failed", props->local_id_);
return -1;
}
const auto& displays = devices_.display_info_list();
for (size_t index = 0; index < displays.size(); ++index) {
const std::string stream_id = MakeDisplayStreamId(index);
AddVideoStream(props->peer_, stream_id.c_str());
}
AddAudioStream(props->peer_, props->audio_label_.c_str());
AddDataStream(props->peer_, props->data_label_.c_str(), false);
AddDataStream(props->peer_, props->mouse_label_.c_str(), false);
AddDataStream(props->peer_, props->keyboard_label_.c_str(), true);
AddDataStream(props->peer_, props->control_data_label_.c_str(), true);
AddDataStream(props->peer_, props->file_label_.c_str(), true);
AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true);
AddDataStream(props->peer_, props->clipboard_label_.c_str(), true);
props->connection_status_.store(ConnectionStatus::Connecting);
peer_to_init = props->peer_;
local_id = props->local_id_;
}
}
if (peer_to_init) {
LOG_INFO("[{}] Create peer instance successful", local_id);
Init(peer_to_init);
LOG_INFO("[{}] Peer init finish", local_id);
}
}
int ret = -1;
// std::shared_lock read_lock(remote_sessions_mutex_);
auto props = remote_sessions_[remote_id];
if (!props->connection_established_) {
props->connection_status_.store(ConnectionStatus::Connecting);
show_connection_status_window_ = true;
props->remember_password_ = remember_password;
if (strcmp(password, "") != 0 &&
strcmp(password, props->remote_password_) != 0) {
strncpy(props->remote_password_, password,
sizeof(props->remote_password_) - 1);
props->remote_password_[sizeof(props->remote_password_) - 1] = '\0';
}
std::string remote_id_with_pwd = remote_id + "@" + password;
if (props->peer_) {
ret = JoinConnection(props->peer_, remote_id_with_pwd.c_str());
if (0 == ret) {
props->rejoin_ = false;
} else {
props->rejoin_ = true;
need_to_rejoin_ = true;
}
}
}
// read_lock.unlock();
return 0;
}
} // namespace crossdesk
@@ -0,0 +1,67 @@
#ifndef _CURSOR_POSITION_H_
#define _CURSOR_POSITION_H_
#include <algorithm>
#include <vector>
#include <remote_action.h>
#include "display_info.h"
namespace crossdesk {
inline void ResetCursorPosition(CursorState* state) {
if (!state) return;
state->position_update = true;
state->position_valid = false;
state->x = 0.5f;
state->y = 0.5f;
state->visual_offset_x = 0.0f;
state->visual_offset_y = 0.0f;
state->display_id = -1;
}
inline bool NormalizeCursorPosition(
double screen_x, double screen_y,
const std::vector<DisplayInfo>& displays, int preferred_display,
CursorState* state) {
if (!state) return false;
ResetCursorPosition(state);
auto contains = [&](int index) {
if (index < 0 || index >= static_cast<int>(displays.size())) return false;
const auto& display = displays[index];
return display.width > 0 && display.height > 0 &&
screen_x >= display.left && screen_x < display.right &&
screen_y >= display.top && screen_y < display.bottom;
};
int display_id = contains(preferred_display) ? preferred_display : -1;
if (display_id < 0) {
for (int index = 0; index < static_cast<int>(displays.size()); ++index) {
if (contains(index)) {
display_id = index;
break;
}
}
}
if (display_id < 0) return false;
const auto& display = displays[display_id];
// Screen coordinates describe a continuous rectangle with an exclusive
// right/bottom edge. Use its full extent so feedback is the exact inverse of
// normalized input instead of accumulating a one-pixel edge convention.
const double horizontal_extent = std::max(display.width, 1);
const double vertical_extent = std::max(display.height, 1);
state->position_valid = true;
state->x = static_cast<float>(std::clamp(
(screen_x - display.left) / horizontal_extent, 0.0, 1.0));
state->y = static_cast<float>(std::clamp(
(screen_y - display.top) / vertical_extent, 0.0, 1.0));
state->display_id = display_id;
return true;
}
} // namespace crossdesk
#endif
@@ -0,0 +1,136 @@
#include "runtime/cursor_state_provider.h"
#include <remote_action.h>
#if defined(_WIN32)
#include <windows.h>
#include "runtime/cursor_position.h"
namespace crossdesk {
namespace {
bool IsSystemCursor(HCURSOR cursor, LPCWSTR resource) {
return cursor != nullptr && cursor == LoadCursorW(nullptr, resource);
}
RemoteCursorShape ShapeFromWindowsCursor(HCURSOR cursor) {
if (IsSystemCursor(cursor, IDC_HELP)) return RemoteCursorShape::help;
if (IsSystemCursor(cursor, IDC_HAND)) return RemoteCursorShape::pointer;
if (IsSystemCursor(cursor, IDC_APPSTARTING))
return RemoteCursorShape::progress;
if (IsSystemCursor(cursor, IDC_WAIT)) return RemoteCursorShape::wait;
if (IsSystemCursor(cursor, IDC_CROSS)) return RemoteCursorShape::crosshair;
if (IsSystemCursor(cursor, IDC_IBEAM)) return RemoteCursorShape::text;
if (IsSystemCursor(cursor, IDC_NO))
return RemoteCursorShape::not_allowed;
if (IsSystemCursor(cursor, IDC_SIZEALL)) return RemoteCursorShape::move;
if (IsSystemCursor(cursor, IDC_SIZEWE)) return RemoteCursorShape::ew_resize;
if (IsSystemCursor(cursor, IDC_SIZENS)) return RemoteCursorShape::ns_resize;
if (IsSystemCursor(cursor, IDC_SIZENESW))
return RemoteCursorShape::nesw_resize;
if (IsSystemCursor(cursor, IDC_SIZENWSE))
return RemoteCursorShape::nwse_resize;
if (IsSystemCursor(cursor, IDC_UPARROW)) return RemoteCursorShape::n_resize;
return RemoteCursorShape::default_cursor;
}
} // namespace
struct CursorStateProvider::Impl {};
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
CursorStateProvider::~CursorStateProvider() = default;
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state) {
if (!state) return false;
CURSORINFO info{};
info.cbSize = sizeof(info);
if (!GetCursorInfo(&info)) return false;
state->seq = 0;
state->visible = (info.flags & CURSOR_SHOWING) != 0;
state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor)
: RemoteCursorShape::none;
NormalizeCursorPosition(info.ptScreenPos.x, info.ptScreenPos.y, displays,
preferred_display, state);
return true;
}
} // namespace crossdesk
#elif defined(__linux__) && !defined(__APPLE__)
#include <X11/Xlib.h>
#include <X11/extensions/Xfixes.h>
#include <string>
#include "linux_cursor_shape.h"
#include "platform.h"
#include "shared_cursor_state.h"
#include "runtime/cursor_position.h"
namespace crossdesk {
namespace {
bool CursorHasVisiblePixel(const XFixesCursorImage& image) {
const size_t pixel_count =
static_cast<size_t>(image.width) * static_cast<size_t>(image.height);
for (size_t index = 0; index < pixel_count; ++index) {
if (((image.pixels[index] >> 24U) & 0xffU) != 0) return true;
}
return false;
}
} // namespace
struct CursorStateProvider::Impl {
Display* display = XOpenDisplay(nullptr);
~Impl() {
if (display) XCloseDisplay(display);
}
};
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
CursorStateProvider::~CursorStateProvider() = default;
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state) {
if (!state || !impl_) return false;
ResetCursorPosition(state);
if (IsWaylandSession()) {
SharedCursorState shared{};
if (GetSharedCursorState(&shared)) {
state->seq = 0;
state->visible = shared.visible;
state->shape = shared.visible ? shared.shape : RemoteCursorShape::none;
return true;
}
}
if (!impl_->display) return false;
XFixesCursorImage* image = XFixesGetCursorImage(impl_->display);
if (!image) return false;
const std::string name = image->name ? image->name : "";
state->seq = 0;
state->visible = CursorHasVisiblePixel(*image);
state->shape = state->visible ? ShapeFromLinuxCursorName(name)
: RemoteCursorShape::none;
NormalizeCursorPosition(image->x, image->y, displays, preferred_display,
state);
XFree(image);
return true;
}
} // namespace crossdesk
#endif
@@ -0,0 +1,33 @@
#ifndef _CURSOR_STATE_PROVIDER_H_
#define _CURSOR_STATE_PROVIDER_H_
#include <memory>
#include <vector>
#include <remote_action.h>
#include "display_info.h"
namespace crossdesk {
// Samples the cursor that is currently displayed by the controlled desktop
// and converts platform-specific cursor handles to protocol cursor shapes.
class CursorStateProvider {
public:
CursorStateProvider();
~CursorStateProvider();
CursorStateProvider(const CursorStateProvider&) = delete;
CursorStateProvider& operator=(const CursorStateProvider&) = delete;
bool Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace crossdesk
#endif
@@ -0,0 +1,41 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-28
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#define CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#include <mutex>
#include <string>
#include <unordered_map>
namespace crossdesk {
class DevicePresenceCache {
public:
void SetOnline(const std::string &device_id, bool online) {
std::lock_guard<std::mutex> lock(mutex_);
cache_[device_id] = online;
}
bool IsOnline(const std::string &device_id) const {
std::lock_guard<std::mutex> lock(mutex_);
const auto it = cache_.find(device_id);
return it != cache_.end() && it->second;
}
void Clear() {
std::lock_guard<std::mutex> lock(mutex_);
cache_.clear();
}
private:
std::unordered_map<std::string, bool> cache_;
mutable std::mutex mutex_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
@@ -0,0 +1,300 @@
#include "runtime/gui_runtime.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include <vector>
#include <display_stream_id.h>
#include "localization.h"
#include "platform/video_renderer.h"
#include "rd_log.h"
namespace crossdesk {
GuiRuntime::GuiRuntime()
: clipboard_(*this), devices_(*this), transfers_(*this), settings_(*this),
keyboard_(*this), peer_events_(*this) {}
GuiRuntime::~GuiRuntime() = default;
int GuiRuntime::CreateConnectionPeer() {
params_.use_cfg_file = false;
std::string signal_server_ip;
int signal_server_port;
int coturn_server_port;
if (config_center_->IsSelfHosted()) {
signal_server_ip = config_center_->GetSignalServerHost();
signal_server_port = config_center_->GetSignalServerPort();
coturn_server_port = config_center_->GetCoturnServerPort();
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();
coturn_server_port = config_center_->GetDefaultCoturnServerPort();
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<std::mutex> 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_;
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';
}
int coturn_port = config_center_->GetCoturnServerPort();
if (coturn_port > 0) {
strncpy(coturn_server_port_self_, std::to_string(coturn_port).c_str(),
sizeof(coturn_server_port_self_) - 1);
coturn_server_port_self_[sizeof(coturn_server_port_self_) - 1] = '\0';
} else {
coturn_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((char *)params_.stun_server_ip, signal_server_ip.c_str(),
sizeof(params_.stun_server_ip) - 1);
params_.stun_server_ip[sizeof(params_.stun_server_ip) - 1] = '\0';
params_.stun_server_port = coturn_server_port;
strncpy((char *)params_.turn_server_ip, signal_server_ip.c_str(),
sizeof(params_.turn_server_ip) - 1);
params_.turn_server_ip[sizeof(params_.turn_server_ip) - 1] = '\0';
params_.turn_server_port = coturn_server_port;
// TURN credentials are issued by the signaling server after login. Keep the
// initial values empty so a reusable static password is never embedded in
// the client binary.
params_.turn_server_username[0] = '\0';
params_.turn_server_password[0] = '\0';
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();
// The Slint desktop renderer currently consumes packed CPU frames. Native
// renderers can opt into platform-native frame output independently.
params_.native_video_output = false;
params_.av1_encoding = config_center_->GetVideoEncodeFormat() ==
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1
? true
: false;
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;
params_.video_degradation_preference =
VideoDegradationPreference::MaintainFrameRate;
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_;
// 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;
signal_status_ = SignalStatus::SignalConnecting;
peer_ = CreatePeer(&params_);
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(), false);
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::UpdateLabels() {
if (!label_inited_ ||
localization_language_index_last_ != localization_language_index_) {
connect_button_label_ =
connect_button_pressed_
? localization::disconnect[localization_language_index_]
: localization::connect[localization_language_index_];
label_inited_ = true;
localization_language_index_last_ = localization_language_index_;
}
}
void GuiRuntime::HandleRecentConnections() {
if (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<GuiRuntime *>(userdata);
if (!runtime) {
return;
}
if (1) {
std::shared_lock lock(runtime->remote_sessions_mutex_);
for (const auto &it : runtime->remote_sessions_) {
auto props = it.second;
if (props->connection_status_.load() == ConnectionStatus::Connected) {
if (props->peer_) {
SendAudioFrame(props->peer_, (const char *)stream, len,
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) {
// GuiApplication *runtime = (GuiApplication *)userdata;
// for (auto it : runtime->remote_sessions_) {
// auto props = it.second;
// if (props->connection_status_ == SignalStatus::SignalConnected) {
// SendAudioFrame(props->peer_, (const char *)stream, len);
// }
// }
// if (!runtime->audio_buffer_fresh_) {
// return;
// }
// SDL_memset(stream, 0, len);
// if (runtime->audio_len_ == 0) {
// return;
// } else {
// }
// len = (len > runtime->audio_len_ ? runtime->audio_len_ : len);
// SDL_MixAudioFormat(stream, runtime->audio_buffer_, AUDIO_S16LSB, len,
// SDL_MIX_MAXVOLUME);
// runtime->audio_buffer_fresh_ = false;
}
} // namespace crossdesk
+101
View File
@@ -0,0 +1,101 @@
#ifndef CROSSDESK_GUI_RUNTIME_H_
#define CROSSDESK_GUI_RUNTIME_H_
#include <atomic>
#include <memory>
#include <string>
#include "features/clipboard/clipboard_controller.h"
#include "features/devices/session_device_manager.h"
#include "features/file_transfer/file_transfer_manager.h"
#include "features/input/keyboard_controller.h"
#include "features/settings/settings_manager.h"
#include "runtime/gui_state.h"
#include "runtime/peer_event_handler.h"
namespace crossdesk {
class VideoRenderer;
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
// state, but no window lifecycle, ImGui view, or transport callback methods.
class GuiRuntime : protected gui_detail::GuiState {
protected:
using FileTransferState = gui_detail::FileTransferState;
using RemoteSession = gui_detail::RemoteSession;
enum class RemoteUnlockState {
none,
service_unavailable,
lock_screen,
credential_ui,
secure_desktop,
};
GuiRuntime();
~GuiRuntime();
static void SdlCaptureAudioIn(void* userdata, Uint8* stream, int len);
static void SdlCaptureAudioOut(void* userdata, Uint8* stream, int len);
int CreateConnectionPeer();
int ConnectTo(const std::string& remote_id, const char* password,
bool remember_password, bool bypass_presence_check = false);
int RequestSingleDevicePresence(const std::string& remote_id,
const char* password, bool remember_password);
void UpdateLabels();
void HandleRecentConnections();
void HandleConnectionStatusChange();
void HandlePendingPresenceProbe();
void HandlePresenceProbeTimeout();
void HandleServerControllerDisconnected(const std::string& remote_id,
const char* reason);
void HandleWindowsServiceIntegration();
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
void CloseAllRemoteSessions();
void ResetRemoteSessionResources(std::shared_ptr<RemoteSession> props);
void WaitForThumbnailSaveTasks();
std::shared_ptr<RemoteSession> FindRemoteSession(
const std::string& remote_id);
void ResetRemoteServiceStatus(RemoteSession& props);
void ApplyRemoteServiceStatus(RemoteSession& props,
const ServiceStatus& status);
RemoteUnlockState GetRemoteUnlockState(const RemoteSession& props) const;
#if _WIN32
void ResetLocalWindowsServiceState(bool clear_pending_sas);
#endif
#ifdef __APPLE__
bool CheckScreenRecordingPermission();
bool CheckAccessibilityPermission();
void OpenScreenRecordingPreferences();
void OpenAccessibilityPreferences();
void RefreshMacPermissionStatus(bool force);
bool EnsureMacScreenRecordingPermission();
bool EnsureMacAccessibilityPermission();
#endif
ClipboardController clipboard_;
SessionDeviceManager devices_;
FileTransferManager transfers_;
SettingsManager settings_;
KeyboardController keyboard_;
PeerEventHandler peer_events_;
std::atomic<bool> video_frame_dirty_{false};
std::unique_ptr<VideoRenderer> video_renderer_;
private:
friend class ClipboardController;
friend class SessionDeviceManager;
friend class FileTransferManager;
friend class SettingsManager;
friend class KeyboardController;
friend class PeerEventHandler;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_RUNTIME_H_
+21
View File
@@ -0,0 +1,21 @@
/*
* Aggregate state visible to GuiRuntime and GuiApplication.
*
* Definitions live beside their owners: SDL/window state in application and
* connection/session state in runtime. This header is intentionally only the
* composition point.
*/
#ifndef CROSSDESK_GUI_STATE_H_
#define CROSSDESK_GUI_STATE_H_
#include "application/application_state.h"
#include "runtime/runtime_state.h"
namespace crossdesk::gui_detail {
struct GuiState : ApplicationState, RuntimeState {};
} // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_STATE_H_
@@ -0,0 +1,319 @@
#include "runtime/peer_event_handler.h"
#include <remote_action.h>
#include <algorithm>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include "device_controller.h"
#include "file_transfer.h"
#include "localization.h"
#include "filesystem_utf8.h"
#include "platform.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#include "runtime/remote_action_codec.h"
#if _WIN32
#include "interactive_state.h"
#include "service_host.h"
#endif
namespace crossdesk {
namespace {
#if _WIN32
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
bool BuildAbsoluteMousePosition(const std::vector<DisplayInfo> &displays,
int display_index, float normalized_x,
float normalized_y, int *absolute_x_out,
int *absolute_y_out) {
if (absolute_x_out == nullptr || absolute_y_out == nullptr ||
display_index < 0 || display_index >= static_cast<int>(displays.size())) {
return false;
}
const DisplayInfo &display = displays[display_index];
if (display.width <= 0 || display.height <= 0) {
return false;
}
const float clamped_x = std::clamp(normalized_x, 0.0f, 1.0f);
const float clamped_y = std::clamp(normalized_y, 0.0f, 1.0f);
*absolute_x_out = static_cast<int>(clamped_x * display.width) + display.left;
*absolute_y_out = static_cast<int>(clamped_y * display.height) + display.top;
return true;
}
void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *side,
const char *stage) {
if (last_tick == nullptr) {
return;
}
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) {
return;
}
*last_tick = now;
LOG_WARN("{} secure-desktop input blocked, stage={}, normal SendInput path "
"cannot drive the Windows password UI",
side != nullptr ? side : "unknown", stage != nullptr ? stage : "");
}
#endif
} // namespace
void PeerEventHandler::OnReceiveDataBuffer(
const char *data, size_t size, const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size, void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
std::string source_id = std::string(src_id, src_id_size);
if (source_id == runtime->file_label_) {
std::string remote_user_id = std::string(user_id, user_id_size);
static FileReceiver receiver;
// Update output directory from config
std::string configured_path =
runtime->config_center_->GetFileTransferSavePath();
if (!configured_path.empty()) {
receiver.SetOutputDir(PathFromUtf8(configured_path));
} else if (receiver.OutputDir().empty()) {
receiver = FileReceiver(); // re-init with default desktop path
}
receiver.SetOnSendAck([runtime,
remote_user_id](const FileTransferAck &ack) -> int {
const auto encoded_ack = EncodeFileTransferAck(ack);
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
if (is_server_sending) {
auto props =
runtime->FindRemoteSession(remote_user_id);
if (props) {
PeerPtr *peer = props->peer_;
return SendReliableDataFrame(
peer, encoded_ack.data(), encoded_ack.size(),
runtime->file_feedback_label_.c_str());
}
}
return SendReliableDataFrame(
runtime->peer_, encoded_ack.data(), encoded_ack.size(),
runtime->file_feedback_label_.c_str());
});
receiver.OnData(data, size);
return;
} else if (source_id == runtime->clipboard_label_) {
if (size > 0) {
std::string remote_user_id(user_id, user_id_size);
auto props =
runtime->FindRemoteSession(remote_user_id);
if (props && !props->enable_mouse_control_) {
return;
}
runtime->clipboard_.QueueRemoteText(data, size);
}
return;
} else if (source_id == runtime->file_feedback_label_) {
runtime->transfers_.HandleAck(data, size);
return;
}
std::string json_str(data, size);
RemoteAction remote_action{};
if (!remote_action.from_json(json_str)) {
LOG_ERROR("Failed to parse RemoteAction JSON payload");
return;
}
std::string remote_id(user_id, user_id_size);
if (remote_action.type == ControlType::service_status) {
auto props_it = runtime->remote_sessions_.find(remote_id);
if (props_it != runtime->remote_sessions_.end()) {
runtime->ApplyRemoteServiceStatus(*props_it->second, remote_action.ss);
}
return;
}
if (remote_action.type == ControlType::service_command) {
#if _WIN32
if (remote_action.c.flag == ServiceCommandFlag::send_sas) {
runtime->pending_windows_service_sas_.store(true,
std::memory_order_relaxed);
} else if (remote_action.c.flag == ServiceCommandFlag::lock_workstation) {
if (!LockWorkStation()) {
LOG_WARN("Remote lock workstation request failed, error={}",
GetLastError());
}
}
#endif
return;
}
if (remote_action.type == ControlType::cursor_state) {
std::shared_ptr<GuiRuntime::RemoteSession> props;
{
std::shared_lock lock(runtime->remote_sessions_mutex_);
auto props_it = runtime->remote_sessions_.find(remote_id);
if (props_it != runtime->remote_sessions_.end()) {
props = props_it->second;
}
}
if (!props) {
return;
}
std::lock_guard lock(props->remote_cursor_state_mutex_);
const uint32_t previous_seq = props->remote_cursor_state_.seq;
const bool is_newer =
!props->remote_cursor_state_received_ ||
static_cast<int32_t>(remote_action.cs.seq - previous_seq) > 0;
if (is_newer) {
const bool changed =
!props->remote_cursor_state_received_ ||
props->remote_cursor_state_.visible != remote_action.cs.visible ||
props->remote_cursor_state_.shape != remote_action.cs.shape;
CursorState merged = remote_action.cs;
if (!remote_action.cs.position_update &&
props->remote_cursor_state_received_) {
merged.position_valid = props->remote_cursor_state_.position_valid;
merged.x = props->remote_cursor_state_.x;
merged.y = props->remote_cursor_state_.y;
merged.display_id = props->remote_cursor_state_.display_id;
}
props->remote_cursor_state_ = merged;
props->remote_cursor_state_received_ = true;
if (changed) {
LOG_INFO("Received cursor state: seq={}, visible={}, shape={}",
remote_action.cs.seq, remote_action.cs.visible,
static_cast<int>(remote_action.cs.shape));
}
}
return;
}
if (remote_action.type == ControlType::host_infomation) {
bool is_client_mode = false;
std::shared_ptr<GuiRuntime::RemoteSession> props;
{
std::shared_lock lock(runtime->remote_sessions_mutex_);
auto props_it = runtime->remote_sessions_.find(remote_id);
if (props_it != runtime->remote_sessions_.end()) {
is_client_mode = true;
props = props_it->second;
}
}
if (is_client_mode) {
// client mode
if (props && props->remote_host_name_.empty()) {
props->remote_host_name_ = std::string(remote_action.i.host_name,
remote_action.i.host_name_size);
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
for (int i = 0; i < remote_action.i.display_num; i++) {
props->display_info_list_.push_back(
DisplayInfo(remote_action.i.display_list[i],
remote_action.i.left[i], remote_action.i.top[i],
remote_action.i.right[i], remote_action.i.bottom[i]));
}
}
remote_action_codec::Free(remote_action);
} else {
// server mode
std::string host_name(remote_action.i.host_name,
remote_action.i.host_name_size);
{
std::unique_lock lock(runtime->connection_status_mutex_);
runtime->connection_host_names_[remote_id] = host_name;
}
LOG_INFO("Remote hostname: [{}]", host_name);
remote_action_codec::Free(remote_action);
}
} else {
// remote
if (runtime->is_server_mode_ &&
remote_action.type == ControlType::mouse) {
std::lock_guard lock(runtime->remote_pointer_input_mutex_);
runtime->last_remote_pointer_input_time_[remote_id] =
std::chrono::steady_clock::now();
}
#if _WIN32
if (runtime->local_service_status_received_ &&
IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_) &&
remote_action.type != ControlType::keyboard &&
remote_action.type != ControlType::keyboard_state) {
if (remote_action.type == ControlType::mouse) {
int absolute_x = 0;
int absolute_y = 0;
if (!BuildAbsoluteMousePosition(runtime->devices_.display_info_list(),
runtime->selected_display_,
remote_action.m.x, remote_action.m.y,
&absolute_x, &absolute_y)) {
LOG_WARN("Secure desktop mouse injection skipped, invalid display "
"mapping: display_index={}, x={}, y={}",
runtime->selected_display_, remote_action.m.x,
remote_action.m.y);
return;
}
const std::string response = SendCrossDeskSecureDesktopMouseInput(
absolute_x, absolute_y, remote_action.m.s,
static_cast<int>(remote_action.m.flag), 1000);
auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.value("ok", false)) {
LogSecureDesktopInputBlocked(
&runtime->last_local_secure_input_block_log_tick_, "local",
runtime->local_interactive_stage_.c_str());
LOG_WARN(
"Secure desktop mouse injection failed, x={}, y={}, wheel={}, "
"flag={}, response={}",
absolute_x, absolute_y, remote_action.m.s,
static_cast<int>(remote_action.m.flag), response);
}
return;
}
}
#endif
if (remote_action.type == ControlType::mouse) {
runtime->devices_.SendMouseCommand(remote_action,
runtime->selected_display_);
} else if (remote_action.type == ControlType::audio_capture) {
if (remote_action.a && !runtime->start_speaker_capturer_)
runtime->devices_.StartSpeakerCapturer();
else if (!remote_action.a && runtime->start_speaker_capturer_)
runtime->devices_.StopSpeakerCapturer();
} else if (remote_action.type == ControlType::keyboard) {
runtime->keyboard_.ApplyRemoteEvent(remote_id, remote_action);
} else if (remote_action.type == ControlType::keyboard_state) {
runtime->keyboard_.ApplyRemoteState(remote_id, remote_action);
} else if (remote_action.type == ControlType::display_id) {
const int ret = runtime->devices_.SwitchDisplay(remote_action.d);
if (ret == 0) {
runtime->selected_display_ = remote_action.d;
} else {
LOG_WARN("Display switch skipped, invalid display_id={}",
remote_action.d);
}
}
}
}
} // namespace crossdesk
@@ -0,0 +1,579 @@
#include "runtime/peer_event_handler.h"
#include <remote_action.h>
#include <algorithm>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "device_controller.h"
#include "file_transfer.h"
#include "localization.h"
#include "platform.h"
#include "platform/video_renderer.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#include "runtime/remote_action_codec.h"
#if _WIN32
#include "interactive_state.h"
#include "service_host.h"
#endif
namespace crossdesk {
void PeerEventHandler::SendClientInfo(PeerPtr* peer,
const std::string& client_id) {
if (!peer) {
return;
}
#if defined(_WIN32)
constexpr const char* kClientPlatform = "windows";
#elif defined(__APPLE__)
constexpr const char* kClientPlatform = "macos";
#elif defined(__linux__)
constexpr const char* kClientPlatform = "linux";
#else
constexpr const char* kClientPlatform = "unknown";
#endif
const nlohmann::json message = {{"type", "client_info"},
{"version", CROSSDESK_VERSION},
{"platform", kClientPlatform}};
const std::string payload = message.dump();
if (SendSignalMessage(peer, payload.data(), payload.size()) != 0) {
LOG_WARN("[{}] failed to report client information", client_id);
}
}
PeerEventHandler::PeerEventHandler(GuiRuntime& owner) : owner_(owner) {}
void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime || !message || size == 0) {
return;
}
std::string s(message, size);
auto j = nlohmann::json::parse(s, nullptr, false);
if (j.is_discarded() || !j.contains("type") || !j["type"].is_string()) {
return;
}
std::string type = j["type"].get<std::string>();
if (type == "presence") {
if (j.contains("devices") && j["devices"].is_array()) {
for (auto& dev : j["devices"]) {
if (!dev.is_object()) {
continue;
}
if (!dev.contains("id") || !dev["id"].is_string()) {
continue;
}
if (!dev.contains("online") || !dev["online"].is_boolean()) {
continue;
}
std::string id = dev["id"].get<std::string>();
bool online = dev["online"].get<bool>();
runtime->device_presence_cache_.SetOnline(id, online);
{
std::lock_guard<std::mutex> lock(
runtime->pending_presence_probe_mutex_);
if (runtime->pending_presence_probe_ &&
runtime->pending_presence_remote_id_ == id) {
runtime->pending_presence_result_ready_ = true;
runtime->pending_presence_online_ = online;
}
}
}
}
} else if (type == "presence_update") {
if (j.contains("id") && j["id"].is_string() && j.contains("online") &&
j["online"].is_boolean()) {
std::string id = j["id"].get<std::string>();
bool online = j["online"].get<bool>();
if (!id.empty()) {
runtime->device_presence_cache_.SetOnline(id, online);
{
std::lock_guard<std::mutex> lock(
runtime->pending_presence_probe_mutex_);
if (runtime->pending_presence_probe_ &&
runtime->pending_presence_remote_id_ == id) {
runtime->pending_presence_result_ready_ = true;
runtime->pending_presence_online_ = online;
}
}
}
}
} else if (type == "change_password") {
std::lock_guard<std::mutex> lock(runtime->password_change_mutex_);
if (!runtime->password_change_pending_) {
LOG_WARN("Ignore unexpected password change response");
return;
}
if (!j.contains("request_id") || !j["request_id"].is_string() ||
j["request_id"].get<std::string>() !=
runtime->pending_password_change_request_id_) {
LOG_WARN("Ignore password change response with unexpected request id");
return;
}
if (j.contains("user_id") && j["user_id"].is_string()) {
const std::string response_user_id = j["user_id"].get<std::string>();
if (!response_user_id.empty() &&
response_user_id != runtime->client_id_) {
LOG_WARN("Ignore password change response for unexpected id [{}]",
response_user_id);
return;
}
}
runtime->password_change_succeeded_ =
j.contains("status") && j["status"].is_string() &&
j["status"].get<std::string>() == "success";
runtime->password_change_error_ =
j.contains("reason") && j["reason"].is_string()
? j["reason"].get<std::string>()
: "Password change failed";
runtime->password_change_result_uncertain_ = false;
runtime->password_change_result_ready_ = true;
}
}
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
size_t user_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
std::string client_id(user_id, user_id_size);
if (client_id == runtime->client_id_) {
runtime->signal_status_ = status;
if (SignalStatus::SignalConnecting == status) {
runtime->signal_connected_ = false;
} else if (SignalStatus::SignalConnected == status) {
runtime->signal_connected_ = true;
runtime->need_to_send_recent_connections_ = true;
LOG_INFO("[{}] connected to signal server", client_id);
SendClientInfo(runtime->peer_, client_id);
std::lock_guard<std::mutex> lock(runtime->password_change_mutex_);
if (runtime->credential_recovery_in_progress_) {
if (runtime->credential_recovery_attempt_pending_) {
runtime->credential_recovery_promote_pending_ = true;
} else {
runtime->credential_recovery_clear_pending_ = true;
}
}
} else if (SignalStatus::SignalFailed == status) {
runtime->signal_connected_ = false;
std::lock_guard<std::mutex> lock(runtime->password_change_mutex_);
if (runtime->credential_recovery_in_progress_ &&
runtime->credential_recovery_attempt_pending_) {
runtime->credential_recovery_retry_active_ = true;
}
} else if (SignalStatus::SignalClosed == status) {
runtime->signal_connected_ = false;
} else if (SignalStatus::SignalReconnecting == status) {
runtime->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) {
runtime->signal_connected_ = false;
} else if (SignalStatus::SignalTlsCertError == status) {
runtime->signal_connected_ = false;
}
} else {
if (client_id.rfind("C-", 0) != 0) {
return;
}
std::string remote_id(client_id.begin() + 2, client_id.end());
// std::shared_lock lock(runtime->remote_sessions_mutex_);
if (runtime->remote_sessions_.find(remote_id) ==
runtime->remote_sessions_.end()) {
return;
}
auto props = runtime->remote_sessions_.find(remote_id)->second;
props->signal_status_ = status;
if (SignalStatus::SignalConnecting == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalConnected == status) {
props->signal_connected_ = true;
LOG_INFO("[{}] connected to signal server", remote_id);
SendClientInfo(props->peer_, client_id);
} else if (SignalStatus::SignalFailed == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalClosed == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalReconnecting == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalTlsCertError == status) {
props->signal_connected_ = false;
}
}
}
void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
const char* user_id,
const size_t user_id_size,
void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) return;
std::string remote_id(user_id, user_id_size);
std::shared_ptr<GuiRuntime::RemoteSession> props;
{
std::shared_lock lock(runtime->remote_sessions_mutex_);
auto it = runtime->remote_sessions_.find(remote_id);
if (it != runtime->remote_sessions_.end()) {
props = it->second;
}
}
if (props) {
runtime->is_client_mode_ = true;
runtime->show_connection_status_window_ = true;
props->connection_status_.store(status);
switch (status) {
case ConnectionStatus::Connected: {
runtime->ResetRemoteServiceStatus(*props);
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
}
{
RemoteAction remote_action;
remote_action.i.display_num =
runtime->devices_.display_info_list().size();
remote_action.i.display_list =
(char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char*)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i
.display_list[i][runtime->devices_.display_info_list()[i]
.name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
runtime->devices_.display_info_list()[i].bottom;
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(),
host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
}
if (!runtime->need_to_create_stream_window_ &&
!runtime->remote_sessions_.empty()) {
runtime->need_to_create_stream_window_ = true;
}
props->connection_established_ = true;
runtime->start_keyboard_capturer_ = true;
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
"connection_closed");
props->connection_established_ = false;
props->enable_mouse_control_ = false;
runtime->ResetRemoteServiceStatus(*props);
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
}
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
int native_snapshot_width = 0;
int native_snapshot_height = 0;
bool needs_native_snapshot = false;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
needs_native_snapshot =
!props->thumbnail_frame_ || props->thumbnail_frame_->empty();
}
if (needs_native_snapshot) {
auto* native_renderer = runtime->video_renderer_.get();
auto snapshot = std::make_shared<std::vector<unsigned char>>();
if (native_renderer &&
native_renderer->CopyLatestNv12(remote_id, snapshot.get(),
&native_snapshot_width,
&native_snapshot_height)) {
native_snapshot = std::move(snapshot);
}
}
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->front_frame_.reset();
props->back_frame_.reset();
if (native_snapshot &&
(!props->thumbnail_frame_ || props->thumbnail_frame_->empty())) {
props->thumbnail_frame_ = std::move(native_snapshot);
props->thumbnail_width_ = native_snapshot_width;
props->thumbnail_height_ = native_snapshot_height;
}
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = true;
}
auto* native_renderer = runtime->video_renderer_.get();
if (native_renderer) {
native_renderer->DiscardStream(remote_id);
runtime->video_frame_dirty_.store(true, std::memory_order_release);
}
runtime->focus_on_stream_window_ = false;
break;
}
case ConnectionStatus::IncorrectPassword: {
runtime->password_validating_ = false;
runtime->password_validating_time_++;
if (runtime->connect_button_pressed_) {
runtime->connect_button_pressed_ = false;
props->connection_established_ = false;
runtime->connect_button_label_ =
localization::connect[runtime->localization_language_index_];
}
break;
}
case ConnectionStatus::NoSuchTransmissionId:
case ConnectionStatus::RemoteUnavailable: {
if (runtime->connect_button_pressed_) {
props->connection_established_ = false;
runtime->connect_button_label_ =
localization::connect[runtime->localization_language_index_];
}
break;
}
default:
break;
}
} else {
runtime->is_client_mode_ = false;
runtime->show_connection_status_window_ = true;
{
std::unique_lock lock(runtime->connection_status_mutex_);
runtime->connection_status_[remote_id] = status;
}
switch (status) {
case ConnectionStatus::Connected: {
#if _WIN32
runtime->last_windows_service_status_tick_ = 0;
#endif
{
RemoteAction remote_action;
remote_action.i.display_num =
runtime->devices_.display_info_list().size();
remote_action.i.display_list =
(char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char*)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i
.display_list[i][runtime->devices_.display_info_list()[i]
.name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
runtime->devices_.display_info_list()[i].bottom;
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(),
host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret =
SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
}
runtime->need_to_destroy_server_window_.store(
false, std::memory_order_release);
runtime->need_to_create_server_window_.store(true,
std::memory_order_release);
runtime->is_server_mode_ = true;
runtime->start_screen_capturer_ = true;
runtime->start_speaker_capturer_ = true;
runtime->remote_client_id_ = remote_id;
runtime->start_mouse_controller_ = true;
{
std::shared_lock lock(runtime->connection_status_mutex_);
if (std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(),
[](const auto& kv) {
return kv.first.find("web") != std::string::npos;
})) {
runtime->show_cursor_ = true;
}
}
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
runtime->HandleServerControllerDisconnected(remote_id,
"connection_closed");
break;
}
default:
break;
}
}
}
void PeerEventHandler::OnNetStatusReport(
const char* client_id, size_t client_id_size, TraversalMode mode,
const XNetTrafficStats* net_traffic_stats, const char* user_id,
const size_t user_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) {
std::string id, password;
const char* at_pos = strchr(client_id, '@');
if (at_pos == nullptr) {
id = client_id;
password.clear();
} else {
id.assign(client_id, at_pos - client_id);
password = at_pos + 1;
}
bool is_self_hosted = runtime->config_center_->IsSelfHosted();
if (is_self_hosted) {
memset(&runtime->client_id_, 0, sizeof(runtime->client_id_));
strncpy(runtime->client_id_, id.c_str(), sizeof(runtime->client_id_) - 1);
runtime->client_id_[sizeof(runtime->client_id_) - 1] = '\0';
memset(&runtime->password_saved_, 0, sizeof(runtime->password_saved_));
strncpy(runtime->password_saved_, password.c_str(),
sizeof(runtime->password_saved_) - 1);
runtime->password_saved_[sizeof(runtime->password_saved_) - 1] = '\0';
memset(&runtime->self_hosted_id_, 0, sizeof(runtime->self_hosted_id_));
strncpy(runtime->self_hosted_id_, client_id,
sizeof(runtime->self_hosted_id_) - 1);
runtime->self_hosted_id_[sizeof(runtime->self_hosted_id_) - 1] = '\0';
LOG_INFO("Use self-hosted client id [{}] and save to cache file", id);
runtime->settings_.PersistSelfHostedIdentity(client_id);
} else {
memset(&runtime->client_id_, 0, sizeof(runtime->client_id_));
strncpy(runtime->client_id_, id.c_str(), sizeof(runtime->client_id_) - 1);
runtime->client_id_[sizeof(runtime->client_id_) - 1] = '\0';
memset(&runtime->password_saved_, 0, sizeof(runtime->password_saved_));
strncpy(runtime->password_saved_, password.c_str(),
sizeof(runtime->password_saved_) - 1);
runtime->password_saved_[sizeof(runtime->password_saved_) - 1] = '\0';
memset(&runtime->client_id_with_password_, 0,
sizeof(runtime->client_id_with_password_));
strncpy(runtime->client_id_with_password_, client_id,
sizeof(runtime->client_id_with_password_) - 1);
runtime
->client_id_with_password_[sizeof(runtime->client_id_with_password_) -
1] = '\0';
LOG_INFO("Use client id [{}] and save id into cache file", id);
runtime->settings_.Save();
}
}
std::string remote_id(user_id, user_id_size);
// std::shared_lock lock(runtime->remote_sessions_mutex_);
if (runtime->remote_sessions_.find(remote_id) ==
runtime->remote_sessions_.end()) {
return;
}
auto props = runtime->remote_sessions_.find(remote_id)->second;
if (props->traversal_mode_ != mode) {
props->traversal_mode_ = mode;
LOG_INFO("Net mode: [{}]", int(props->traversal_mode_));
}
if (!net_traffic_stats) {
return;
}
// only display client side net status if connected to itself
if (!(runtime->peer_reserved_ && !strstr(client_id, "C-"))) {
props->net_traffic_stats_ = *net_traffic_stats;
}
}
} // namespace crossdesk
@@ -0,0 +1,50 @@
#ifndef CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
#define CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
#include <cstddef>
#include <string>
#include "minirtc.h"
namespace crossdesk {
class GuiRuntime;
// Adapts MiniRTC C callbacks to the GUI runtime.
class PeerEventHandler {
public:
explicit PeerEventHandler(GuiRuntime &owner);
static void OnReceiveVideoBuffer(const XVideoFrame *video_frame,
const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size,
void *user_data);
static void OnReceiveAudioBuffer(const char *data, size_t size,
const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size,
void *user_data);
static void OnReceiveDataBuffer(const char *data, size_t size,
const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size,
void *user_data);
static void OnSignalStatus(SignalStatus status, const char *user_id,
size_t user_id_size, void *user_data);
static void OnSignalMessage(const char *message, size_t size,
void *user_data);
static void OnConnectionStatus(ConnectionStatus status, const char *user_id,
size_t user_id_size, void *user_data);
static void OnNetStatusReport(const char *client_id, size_t client_id_size,
TraversalMode mode,
const XNetTrafficStats *net_traffic_stats,
const char *user_id, size_t user_id_size,
void *user_data);
private:
static void SendClientInfo(PeerPtr *peer, const std::string &client_id);
GuiRuntime &owner_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
@@ -0,0 +1,145 @@
#include <chrono>
#include <cstring>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "platform/video_renderer.h"
#include "runtime/gui_runtime.h"
#include "runtime/peer_event_handler.h"
namespace crossdesk {
namespace {
constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1);
} // namespace
void PeerEventHandler::OnReceiveVideoBuffer(
const XVideoFrame* video_frame, const char* user_id, size_t user_id_size,
const char* src_id, size_t src_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
std::string remote_id(user_id, user_id_size);
// std::shared_lock lock(runtime->remote_sessions_mutex_);
if (runtime->remote_sessions_.find(remote_id) ==
runtime->remote_sessions_.end()) {
return;
}
GuiRuntime::RemoteSession* props =
runtime->remote_sessions_.find(remote_id)->second.get();
if (props->connection_established_) {
bool background_snapshot_only = false;
auto* native_renderer = runtime->video_renderer_.get();
if (native_renderer && native_renderer->IsActive()) {
const auto submit_result = native_renderer->SubmitNv12(
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
video_frame->size, video_frame->width, video_frame->height);
if (submit_result == VideoRenderer::SubmitResult::submitted) {
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
const bool size_changed = (props->video_width_ != video_frame->width) ||
(props->video_height_ != video_frame->height);
if (size_changed) {
props->render_rect_dirty_ = true;
}
props->video_width_ = video_frame->width;
props->video_height_ = video_frame->height;
props->video_size_ = video_frame->size;
// Once the native renderer owns the current stream, do not leave an
// older CPU frame ahead of the renderer's close snapshot.
props->front_frame_.reset();
props->back_frame_.reset();
props->thumbnail_frame_.reset();
props->thumbnail_width_ = 0;
props->thumbnail_height_ = 0;
props->background_snapshot_time_ = {};
++props->video_frame_sequence_;
props->streaming_ = true;
runtime->video_frame_dirty_.store(true, std::memory_order_release);
return;
}
if (submit_result == VideoRenderer::SubmitResult::dropped ||
submit_result == VideoRenderer::SubmitResult::failed) {
// Keep presenting the last native frame. A later decoded frame can
// reuse the renderer without changing ownership mid-window.
props->streaming_ = true;
return;
}
if (submit_result == VideoRenderer::SubmitResult::not_selected) {
background_snapshot_only = true;
}
}
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
const auto now = std::chrono::steady_clock::now();
if (background_snapshot_only && props->thumbnail_frame_ &&
!props->thumbnail_frame_->empty() &&
props->background_snapshot_time_ !=
std::chrono::steady_clock::time_point{} &&
now - props->background_snapshot_time_ <
kBackgroundSnapshotInterval) {
props->streaming_ = true;
return;
}
// Allocate a third buffer only while the UI still owns the old snapshot.
if (!props->back_frame_ || props->back_frame_.use_count() != 1) {
props->back_frame_ =
std::make_shared<std::vector<unsigned char>>(video_frame->size);
}
if (props->back_frame_->size() != video_frame->size) {
props->back_frame_->resize(video_frame->size);
}
std::memcpy(props->back_frame_->data(), video_frame->data,
video_frame->size);
const bool size_changed = (props->video_width_ != video_frame->width) ||
(props->video_height_ != video_frame->height);
if (size_changed) {
props->render_rect_dirty_ = true;
}
props->video_width_ = video_frame->width;
props->video_height_ = video_frame->height;
props->video_size_ = video_frame->size;
props->front_frame_.swap(props->back_frame_);
props->thumbnail_frame_ = props->front_frame_;
props->thumbnail_width_ = video_frame->width;
props->thumbnail_height_ = video_frame->height;
if (background_snapshot_only) {
props->background_snapshot_time_ = now;
}
++props->video_frame_sequence_;
}
props->streaming_ = true;
if (background_snapshot_only) {
return;
}
runtime->video_frame_dirty_.store(true, std::memory_order_release);
}
}
void PeerEventHandler::OnReceiveAudioBuffer(
const char* data, size_t size, const char* user_id, size_t user_id_size,
const char* src_id, size_t src_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
runtime->audio_buffer_fresh_ = true;
runtime->devices_.PushAudio(data, size);
}
} // namespace crossdesk
@@ -0,0 +1,119 @@
#include "runtime/remote_action_codec.h"
#include <remote_action.h>
#include <cstdlib>
#include <cstring>
namespace crossdesk::remote_action_codec {
std::vector<char> Serialize(const RemoteAction &action) {
std::vector<char> buffer;
buffer.push_back(static_cast<char>(action.type));
auto insert_bytes = [&](const void *ptr, size_t len) {
buffer.insert(buffer.end(), static_cast<const char *>(ptr),
static_cast<const char *>(ptr) + len);
};
if (action.type == ControlType::host_infomation) {
insert_bytes(&action.i.host_name_size, sizeof(size_t));
insert_bytes(action.i.host_name, action.i.host_name_size);
size_t num = action.i.display_num;
insert_bytes(&num, sizeof(size_t));
for (size_t i = 0; i < num; ++i) {
const size_t len = std::strlen(action.i.display_list[i]);
insert_bytes(&len, sizeof(size_t));
insert_bytes(action.i.display_list[i], len);
}
insert_bytes(action.i.left, sizeof(int) * num);
insert_bytes(action.i.top, sizeof(int) * num);
insert_bytes(action.i.right, sizeof(int) * num);
insert_bytes(action.i.bottom, sizeof(int) * num);
}
return buffer;
}
bool Deserialize(const char *data, size_t size, RemoteAction &out) {
size_t offset = 0;
auto read = [&](void *dst, size_t len) -> bool {
if (offset + len > size) {
return false;
}
std::memcpy(dst, data + offset, len);
offset += len;
return true;
};
if (size < 1) {
return false;
}
out.type = static_cast<ControlType>(data[offset++]);
if (out.type == ControlType::host_infomation) {
size_t name_len;
if (!read(&name_len, sizeof(size_t)) ||
name_len >= sizeof(out.i.host_name)) {
return false;
}
if (!read(out.i.host_name, name_len)) {
return false;
}
out.i.host_name[name_len] = '\0';
out.i.host_name_size = name_len;
size_t num;
if (!read(&num, sizeof(size_t))) {
return false;
}
out.i.display_num = num;
out.i.display_list =
static_cast<char **>(std::malloc(num * sizeof(char *)));
for (size_t i = 0; i < num; ++i) {
size_t len;
if (!read(&len, sizeof(size_t)) || offset + len > size) {
return false;
}
out.i.display_list[i] = static_cast<char *>(std::malloc(len + 1));
std::memcpy(out.i.display_list[i], data + offset, len);
out.i.display_list[i][len] = '\0';
offset += len;
}
auto alloc_int_array = [&](int *&array) {
array = static_cast<int *>(std::malloc(num * sizeof(int)));
return read(array, num * sizeof(int));
};
return alloc_int_array(out.i.left) && alloc_int_array(out.i.top) &&
alloc_int_array(out.i.right) && alloc_int_array(out.i.bottom);
}
return true;
}
void Free(RemoteAction &action) {
if (action.type != ControlType::host_infomation) {
return;
}
for (size_t i = 0; i < action.i.display_num; ++i) {
std::free(action.i.display_list[i]);
}
std::free(action.i.display_list);
std::free(action.i.left);
std::free(action.i.top);
std::free(action.i.right);
std::free(action.i.bottom);
action.i.display_list = nullptr;
action.i.left = action.i.top = action.i.right = action.i.bottom = nullptr;
action.i.display_num = 0;
}
} // namespace crossdesk::remote_action_codec
@@ -0,0 +1,17 @@
#ifndef CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
#define CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
#include <cstddef>
#include <vector>
#include <remote_action.h>
namespace crossdesk::remote_action_codec {
std::vector<char> Serialize(const RemoteAction &action);
bool Deserialize(const char *data, size_t size, RemoteAction &out);
void Free(RemoteAction &action);
} // namespace crossdesk::remote_action_codec
#endif // CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
@@ -0,0 +1,177 @@
#ifndef _REMOTE_SESSION_H_
#define _REMOTE_SESSION_H_
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <mutex>
#include <optional>
#include <queue>
#include <string>
#include <vector>
#include <remote_action.h>
#include <stream_names.h>
#include "display_info.h"
#include "minirtc.h"
namespace crossdesk::gui_detail {
struct FileTransferState {
std::atomic<bool> file_sending_ = false;
std::atomic<uint64_t> file_sent_bytes_ = 0;
std::atomic<uint64_t> file_total_bytes_ = 0;
std::atomic<uint32_t> file_send_rate_bps_ = 0;
std::mutex file_transfer_mutex_;
std::chrono::steady_clock::time_point file_send_start_time_;
std::chrono::steady_clock::time_point file_send_last_update_time_;
uint64_t file_send_last_bytes_ = 0;
bool file_transfer_window_visible_ = false;
bool file_transfer_window_hovered_ = false;
std::atomic<uint32_t> current_file_id_{0};
struct QueuedFile {
std::filesystem::path file_path;
std::string file_label;
std::string remote_id;
};
std::queue<QueuedFile> file_send_queue_;
std::mutex file_queue_mutex_;
enum class FileTransferStatus { Queued, Sending, Completed, Failed };
struct FileTransferInfo {
std::string file_name;
std::filesystem::path file_path;
uint64_t file_size = 0;
FileTransferStatus status = FileTransferStatus::Queued;
uint64_t sent_bytes = 0;
uint32_t file_id = 0;
uint32_t rate_bps = 0;
};
std::vector<FileTransferInfo> file_transfer_list_;
std::mutex file_transfer_list_mutex_;
};
// Runtime state for one connected or connecting remote endpoint. It groups the
// connection, media, input, window and transfer data that share one lifetime.
struct RemoteSession {
Params params_;
PeerPtr* peer_ = nullptr;
std::string audio_label_ = kAudioStream;
std::string data_label_ = kDataStream;
std::string mouse_label_ = kMouseStream;
std::string keyboard_label_ = kKeyboardStream;
std::string file_label_ = kFileStream;
std::string control_data_label_ = kControlStream;
std::string file_feedback_label_ = kFileFeedbackStream;
std::string clipboard_label_ = kClipboardStream;
std::string local_id_;
std::string remote_id_;
bool exit_ = false;
bool signal_connected_ = false;
SignalStatus signal_status_ = SignalStatus::SignalClosed;
bool connection_established_ = false;
bool rejoin_ = false;
bool net_traffic_stats_button_pressed_ = false;
bool enable_mouse_control_ = true;
bool mouse_controller_is_started_ = false;
bool audio_capture_button_pressed_ = true;
bool control_mouse_ = true;
bool streaming_ = false;
bool is_control_bar_in_left_ = true;
bool control_bar_hovered_ = false;
bool display_selectable_hovered_ = false;
bool shortcut_selectable_hovered_ = false;
bool control_bar_expand_ = true;
bool reset_control_bar_pos_ = false;
bool control_window_width_is_changing_ = false;
bool control_window_height_is_changing_ = false;
bool p2p_mode_ = true;
bool remember_password_ = false;
char remote_password_[7] = "";
float sub_stream_window_width_ = 1280;
float sub_stream_window_height_ = 720;
float control_window_min_width_ = 20;
float control_window_max_width_ = 300;
float control_window_min_height_ = 38;
float control_window_max_height_ = 180;
float control_window_width_ = 300;
float control_window_height_ = 38;
float control_bar_pos_x_ = 0;
float control_bar_pos_y_ = 30;
float mouse_diff_control_bar_pos_x_ = 0;
float mouse_diff_control_bar_pos_y_ = 0;
double control_bar_button_pressed_time_ = 0;
double net_traffic_stats_button_pressed_time_ = 0;
// Written by the decode callback thread and consumed by the UI thread.
std::mutex video_frame_mutex_;
std::shared_ptr<std::vector<unsigned char>> front_frame_;
std::shared_ptr<std::vector<unsigned char>> back_frame_;
// Retains the most recent background CPU frame across disconnect cleanup
// so closing an unselected tab can still update its recent-item thumbnail.
std::shared_ptr<std::vector<unsigned char>> thumbnail_frame_;
int thumbnail_width_ = 0;
int thumbnail_height_ = 0;
std::chrono::steady_clock::time_point background_snapshot_time_;
bool render_rect_dirty_ = false;
bool stream_cleanup_pending_ = false;
float mouse_pos_x_ = 0;
float mouse_pos_y_ = 0;
float mouse_pos_x_last_ = 0;
float mouse_pos_y_last_ = 0;
int texture_width_ = 1280;
int texture_height_ = 720;
int video_width_ = 0;
int video_height_ = 0;
int video_width_last_ = 0;
int video_height_last_ = 0;
int selected_display_ = 0;
size_t video_size_ = 0;
uint64_t video_frame_sequence_ = 0;
bool tab_selected_ = false;
bool tab_opened_ = true;
std::optional<float> pos_x_before_docked_;
std::optional<float> pos_y_before_docked_;
float render_window_x_ = 0;
float render_window_y_ = 0;
float render_window_width_ = 0;
float render_window_height_ = 0;
std::string fullscreen_button_label_ = "Fullscreen";
std::string net_traffic_stats_button_label_ = "Show Net Traffic Stats";
std::string mouse_control_button_label_ = "Mouse Control";
std::string audio_capture_button_label_ = "Audio Capture";
std::string remote_host_name_;
bool remote_service_status_received_ = false;
bool remote_service_available_ = false;
std::string remote_interactive_stage_;
std::vector<DisplayInfo> display_info_list_;
// Cursor snapshots arrive on the transport callback thread and are applied
// by the Slint UI thread. Keep the snapshot atomic as a unit so visibility
// and shape cannot briefly come from different protocol messages.
std::mutex remote_cursor_state_mutex_;
CursorState remote_cursor_state_{};
bool remote_cursor_state_received_ = false;
// Shared by minirtc callbacks, Slint rendering and SDL audio callbacks.
std::atomic<ConnectionStatus> connection_status_ = ConnectionStatus::Closed;
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
int fps_ = 0;
int frame_count_ = 0;
std::chrono::steady_clock::time_point last_time_;
XNetTrafficStats net_traffic_stats_{};
using QueuedFile = FileTransferState::QueuedFile;
using FileTransferStatus = FileTransferState::FileTransferStatus;
using FileTransferInfo = FileTransferState::FileTransferInfo;
FileTransferState file_transfer_;
};
using RemoteSessionPtr = std::shared_ptr<RemoteSession>;
} // namespace crossdesk::gui_detail
#endif
@@ -0,0 +1,213 @@
#ifndef _RUNTIME_STATE_H_
#define _RUNTIME_STATE_H_
#include <atomic>
#include <chrono>
#include <cstdint>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>
#include <stream_names.h>
#include "config_center.h"
#include "path_manager.h"
#include "runtime/device_presence_cache.h"
#include "runtime/remote_session.h"
#include "thumbnail.h"
namespace crossdesk::gui_detail {
struct InfrastructureState {
std::unique_ptr<ConfigCenter> config_center_;
ConfigCenter::LANGUAGE localization_language_ =
ConfigCenter::LANGUAGE::CHINESE;
std::unique_ptr<PathManager> path_manager_;
std::string exec_log_path_;
std::string dll_log_path_;
std::string cache_path_;
int localization_language_index_ = -1;
int localization_language_index_last_ = -1;
bool modules_inited_ = false;
unsigned char aes128_key_[16]{};
unsigned char aes128_iv_[16]{};
};
struct RecentConnectionsState {
std::shared_ptr<Thumbnail> thumbnail_;
std::vector<std::pair<std::string, Thumbnail::RecentConnection>>
recent_connections_;
std::vector<std::string> recent_connection_ids_;
int recent_connection_image_width_ = 160;
int recent_connection_image_height_ = 90;
uint32_t recent_connection_image_save_time_ = 0;
DevicePresenceCache device_presence_cache_;
bool need_to_send_recent_connections_ = true;
};
struct PeerState {
SignalStatus signal_status_ = SignalStatus::SignalClosed;
std::string signal_status_str_;
bool signal_connected_ = false;
PeerPtr *peer_ = nullptr;
PeerPtr *peer_reserved_ = nullptr;
std::string video_primary_label_ = "primary_display";
std::string video_secondary_label_ = "secondary_display";
std::string audio_label_ = "audio";
std::string data_label_ = kDataStream;
std::string mouse_label_ = kMouseStream;
std::string keyboard_label_ = kKeyboardStream;
std::string info_label_ = "info";
std::string control_data_label_ = kControlStream;
std::string file_label_ = kFileStream;
std::string file_feedback_label_ = kFileFeedbackStream;
std::string clipboard_label_ = kClipboardStream;
Params params_;
};
// OS-specific service and permission state is kept separate from transport
// state so platform code does not appear to be part of the wire contract.
struct PlatformIntegrationState {
#if _WIN32
std::atomic<bool> pending_windows_service_sas_{false};
bool local_service_status_received_ = false;
bool local_service_available_ = false;
std::string local_interactive_stage_;
uint32_t last_local_secure_input_block_log_tick_ = 0;
uint32_t last_windows_service_status_tick_ = 0;
uint32_t optimistic_windows_secure_desktop_until_tick_ = 0;
#if CROSSDESK_PORTABLE
enum class PortableServiceInstallState {
idle,
installing,
succeeded,
failed,
};
bool portable_service_prompt_checked_ = false;
bool show_portable_service_install_window_ = false;
bool show_portable_service_prompt_suppressed_window_ = false;
bool portable_service_do_not_remind_ = false;
bool portable_service_prompt_suppressed_ = false;
bool portable_service_installed_ = false;
std::atomic<PortableServiceInstallState> portable_service_install_state_{
PortableServiceInstallState::idle};
std::thread portable_service_install_thread_;
#endif
#endif
#ifdef __APPLE__
bool show_request_permission_window_ = true;
bool mac_permission_status_initialized_ = false;
uint32_t mac_permission_last_check_tick_ = 0;
bool mac_screen_recording_permission_granted_ = false;
bool mac_accessibility_permission_granted_ = false;
bool mac_screen_recording_permission_requested_ = false;
bool mac_accessibility_permission_requested_ = false;
#endif
};
struct UserSettingsState {
char client_id_[10] = "";
char client_id_display_[12] = "";
char client_id_with_password_[17] = "";
char password_saved_[7] = "";
char self_hosted_id_[17] = "";
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_encode_format_button_value_ = 0;
bool enable_hardware_video_codec_ = true;
bool enable_turn_ = true;
bool enable_srtp_ = false;
char signal_server_ip_[256] = "api.crossdesk.cn";
char signal_server_port_[6] = "9099";
char coturn_server_port_[6] = "3478";
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_encode_format_button_value_last_ = 0;
bool enable_hardware_video_codec_last_ = false;
bool enable_turn_last_ = true;
bool enable_srtp_last_ = false;
bool enable_self_hosted_last_ = false;
bool enable_autostart_ = false;
bool enable_autostart_last_ = false;
bool enable_daemon_ = false;
bool enable_daemon_last_ = false;
char file_transfer_save_path_buf_[512] = "";
std::string file_transfer_save_path_last_;
char signal_server_ip_self_[256] = "";
char signal_server_port_self_[6] = "";
char coturn_server_port_self_[6] = "";
bool settings_window_pos_reset_ = true;
bool self_hosted_server_config_window_pos_reset_ = true;
std::string selected_current_file_path_;
bool show_file_browser_ = true;
};
struct PasswordChangeState {
std::mutex password_change_mutex_;
uint64_t next_password_change_request_id_ = 0;
bool password_change_pending_ = false;
bool password_change_result_ready_ = false;
bool password_change_succeeded_ = false;
bool password_change_result_uncertain_ = false;
std::chrono::steady_clock::time_point password_change_requested_at_;
std::string pending_password_change_request_id_;
std::string pending_local_password_;
std::string password_change_error_;
bool credential_recovery_in_progress_ = false;
bool credential_recovery_attempt_pending_ = false;
bool credential_recovery_retry_active_ = false;
bool credential_recovery_promote_pending_ = false;
bool credential_recovery_clear_pending_ = false;
};
struct ConnectionState {
using RemoteSessionMap =
std::unordered_map<std::string, RemoteSessionPtr>;
RemoteSessionMap remote_sessions_;
std::shared_mutex remote_sessions_mutex_;
std::vector<std::thread> thumbnail_save_threads_;
std::mutex thumbnail_save_threads_mutex_;
std::shared_mutex connection_status_mutex_;
std::unordered_map<std::string, ConnectionStatus> connection_status_;
std::unordered_map<std::string, std::string> connection_host_names_;
// Cursor position is sampled asynchronously after remote mouse input has
// been injected. Remember the input source so the sampled position is not
// immediately echoed back to that same controller as stale feedback.
std::mutex remote_pointer_input_mutex_;
std::unordered_map<std::string, std::chrono::steady_clock::time_point>
last_remote_pointer_input_time_;
std::string selected_server_remote_id_;
std::string selected_server_remote_hostname_;
std::mutex pending_presence_probe_mutex_;
bool pending_presence_probe_ = false;
bool pending_presence_result_ready_ = false;
bool pending_presence_online_ = false;
std::chrono::steady_clock::time_point pending_presence_probe_started_at_;
std::string pending_presence_remote_id_;
std::string pending_presence_password_;
bool pending_presence_remember_password_ = false;
};
struct RuntimeState : InfrastructureState,
RecentConnectionsState,
PeerState,
PlatformIntegrationState,
UserSettingsState,
PasswordChangeState,
ConnectionState {};
} // namespace crossdesk::gui_detail
#endif