[feat] add native iOS client support

This commit is contained in:
dijunkun
2026-08-31 02:14:27 +08:00
parent 126f455681
commit 7595ab8bd8
50 changed files with 7391 additions and 432 deletions
+22
View File
@@ -0,0 +1,22 @@
#ifndef _STREAM_NAMES_H_
#define _STREAM_NAMES_H_
#include <cstddef>
namespace crossdesk::protocol {
inline constexpr char kAudioStream[] = "control_audio";
inline constexpr char kDataStream[] = "data";
inline constexpr char kMouseStream[] = "mouse";
inline constexpr char kKeyboardStream[] = "keyboard";
inline constexpr char kControlStream[] = "control_data";
inline constexpr char kFileStream[] = "file";
inline constexpr char kFileFeedbackStream[] = "file_feedback";
inline constexpr char kClipboardStream[] = "clipboard";
inline constexpr std::size_t kFileChunkSize = 64 * 1024;
inline constexpr std::size_t kMaxClipboardBytes = 128 * 1024;
} // namespace crossdesk::protocol
#endif
+59 -233
View File
@@ -7,20 +7,16 @@
#ifndef _DEVICE_CONTROLLER_H_
#define _DEVICE_CONTROLLER_H_
#include <stdio.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <nlohmann/json.hpp>
#include <string>
#include "display_info.h"
#include "remote_cursor_shape.h"
using json = nlohmann::json;
namespace crossdesk {
typedef enum {
enum ControlType {
invalid = -1,
mouse = 0,
keyboard = 1,
audio_capture = 2,
@@ -30,8 +26,9 @@ typedef enum {
service_command = 6,
keyboard_state = 7,
cursor_state = 8,
} ControlType;
typedef enum {
};
enum MouseFlag {
move = 0,
left_down,
left_up,
@@ -40,66 +37,80 @@ typedef enum {
middle_down,
middle_up,
wheel_vertical,
wheel_horizontal
} MouseFlag;
typedef enum { key_down = 0, key_up } KeyFlag;
typedef enum { send_sas = 0, lock_workstation } ServiceCommandFlag;
typedef struct {
wheel_horizontal,
};
enum KeyFlag { key_down = 0, key_up };
enum ServiceCommandFlag { send_sas = 0, lock_workstation };
struct Mouse {
float x;
float y;
int s;
MouseFlag flag;
} Mouse;
};
typedef struct {
size_t key_value;
struct Key {
std::size_t key_value;
uint32_t scan_code;
bool extended;
KeyFlag flag;
} Key;
};
inline constexpr size_t kMaxKeyboardStateKeys = 32;
inline constexpr std::size_t kMaxKeyboardStateKeys = 32;
typedef struct {
size_t key_value;
struct KeyboardStateKey {
std::size_t key_value;
uint32_t scan_code;
bool extended;
} KeyboardStateKey;
};
typedef struct {
struct KeyboardState {
uint32_t seq;
size_t pressed_count;
std::size_t pressed_count;
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
} KeyboardState;
};
typedef struct {
struct CursorState {
uint32_t seq;
bool visible;
RemoteCursorShape shape;
} CursorState;
bool position_valid;
float x;
float y;
// Normalized displacement from the input hotspot to the visible cursor
// anchor. This is presentation metadata and must not affect input mapping.
float visual_offset_x;
float visual_offset_y;
int display_id;
// Whether receivers should apply the position fields in this message.
// Shape-only updates set this to false so cursor appearance can remain
// responsive while position feedback to the input source is suppressed.
bool position_update;
};
typedef struct {
struct HostInfo {
char host_name[64];
size_t host_name_size;
std::size_t host_name_size;
char** display_list;
size_t display_num;
std::size_t display_num;
int* left;
int* top;
int* right;
int* bottom;
} HostInfo;
};
typedef struct {
struct ServiceStatus {
bool available;
char interactive_stage[32];
} ServiceStatus;
};
typedef struct {
struct ServiceCommand {
ServiceCommandFlag flag;
} ServiceCommand;
};
struct RemoteAction {
ControlType type;
ControlType type = ControlType::invalid;
union {
Mouse m;
Key k;
@@ -112,210 +123,25 @@ struct RemoteAction {
ServiceCommand c;
};
// parse
std::string to_json() const { return ToJson(*this); }
std::string to_json() const;
bool from_json(const std::string& json_string);
bool from_json(const std::string& json_str) {
RemoteAction temp;
if (!FromJson(json_str, temp)) return false;
*this = temp;
return true;
}
static std::string ToJson(const RemoteAction& a) {
json j;
j["type"] = a.type;
switch (a.type) {
case ControlType::mouse:
j["mouse"] = {
{"x", a.m.x}, {"y", a.m.y}, {"s", a.m.s}, {"flag", a.m.flag}};
break;
case ControlType::keyboard:
j["keyboard"] = {{"key_value", a.k.key_value},
{"scan_code", a.k.scan_code},
{"extended", a.k.extended},
{"flag", a.k.flag}};
break;
case ControlType::keyboard_state: {
json keys = json::array();
const size_t pressed_count =
a.ks.pressed_count < kMaxKeyboardStateKeys
? a.ks.pressed_count
: kMaxKeyboardStateKeys;
for (size_t idx = 0; idx < pressed_count; ++idx) {
keys.push_back({{"key_value", a.ks.pressed_keys[idx].key_value},
{"scan_code", a.ks.pressed_keys[idx].scan_code},
{"extended", a.ks.pressed_keys[idx].extended}});
}
j["keyboard_state"] = {{"seq", a.ks.seq}, {"pressed_keys", keys}};
break;
}
case ControlType::cursor_state:
j["cursor_state"] = {{"seq", a.cs.seq},
{"visible", a.cs.visible},
{"shape", static_cast<int>(a.cs.shape)}};
break;
case ControlType::audio_capture:
j["audio_capture"] = a.a;
break;
case ControlType::display_id:
j["display_id"] = a.d;
break;
case ControlType::service_status:
j["service_status"] = {{"available", a.ss.available},
{"interactive_stage", a.ss.interactive_stage}};
break;
case ControlType::service_command:
j["service_command"] = {{"flag", a.c.flag}};
break;
case ControlType::host_infomation: {
json displays = json::array();
for (size_t idx = 0; idx < a.i.display_num; idx++) {
displays.push_back(
{{"name", a.i.display_list ? a.i.display_list[idx] : ""},
{"left", a.i.left ? a.i.left[idx] : 0},
{"top", a.i.top ? a.i.top[idx] : 0},
{"right", a.i.right ? a.i.right[idx] : 0},
{"bottom", a.i.bottom ? a.i.bottom[idx] : 0}});
}
j["host_info"] = {{"host_name", a.i.host_name},
{"display_num", a.i.display_num},
{"displays", displays}};
break;
}
}
return j.dump();
}
static bool FromJson(const std::string& json_str, RemoteAction& out) {
try {
json j = json::parse(json_str);
out.type = (ControlType)j.at("type").get<int>();
switch (out.type) {
case ControlType::mouse:
out.m.x = j.at("mouse").at("x").get<float>();
out.m.y = j.at("mouse").at("y").get<float>();
out.m.s = j.at("mouse").at("s").get<int>();
out.m.flag = (MouseFlag)j.at("mouse").at("flag").get<int>();
break;
case ControlType::keyboard:
out.k.key_value = j.at("keyboard").at("key_value").get<size_t>();
out.k.scan_code =
j.at("keyboard").value("scan_code", static_cast<uint32_t>(0));
out.k.extended = j.at("keyboard").value("extended", false);
out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>();
break;
case ControlType::keyboard_state: {
const auto& keyboard_state_json = j.at("keyboard_state");
out.ks.seq = keyboard_state_json.value("seq", 0u);
out.ks.pressed_count = 0;
const auto keys_json =
keyboard_state_json.value("pressed_keys", json::array());
if (!keys_json.is_array()) {
break;
}
const size_t count =
keys_json.size() < kMaxKeyboardStateKeys
? keys_json.size()
: kMaxKeyboardStateKeys;
for (size_t idx = 0; idx < count; ++idx) {
const auto& key_json = keys_json[idx];
out.ks.pressed_keys[idx].key_value =
key_json.at("key_value").get<size_t>();
out.ks.pressed_keys[idx].scan_code =
key_json.value("scan_code", static_cast<uint32_t>(0));
out.ks.pressed_keys[idx].extended =
key_json.value("extended", false);
}
out.ks.pressed_count = count;
break;
}
case ControlType::cursor_state: {
const auto& cursor_state_json = j.at("cursor_state");
const int shape = cursor_state_json.at("shape").get<int>();
if (shape < static_cast<int>(RemoteCursorShape::default_cursor) ||
shape > static_cast<int>(RemoteCursorShape::nwse_resize)) {
return false;
}
out.cs.seq = cursor_state_json.at("seq").get<uint32_t>();
out.cs.visible = cursor_state_json.at("visible").get<bool>();
out.cs.shape = static_cast<RemoteCursorShape>(shape);
break;
}
case ControlType::audio_capture:
out.a = j.at("audio_capture").get<bool>();
break;
case ControlType::display_id:
out.d = j.at("display_id").get<int>();
break;
case ControlType::service_status: {
const auto& service_status_json = j.at("service_status");
out.ss.available = service_status_json.value("available", false);
std::string interactive_stage =
service_status_json.value("interactive_stage", std::string());
std::strncpy(out.ss.interactive_stage, interactive_stage.c_str(),
sizeof(out.ss.interactive_stage) - 1);
out.ss.interactive_stage[sizeof(out.ss.interactive_stage) - 1] = '\0';
break;
}
case ControlType::service_command:
out.c.flag = static_cast<ServiceCommandFlag>(
j.at("service_command").at("flag").get<int>());
break;
case ControlType::host_infomation: {
std::string host_name =
j.at("host_info").at("host_name").get<std::string>();
strncpy(out.i.host_name, host_name.c_str(), sizeof(out.i.host_name));
out.i.host_name[sizeof(out.i.host_name) - 1] = '\0';
out.i.host_name_size = host_name.size();
out.i.display_num = j.at("host_info").at("display_num").get<size_t>();
auto displays = j.at("host_info").at("displays");
out.i.display_list =
(char**)malloc(out.i.display_num * sizeof(char*));
out.i.left = (int*)malloc(out.i.display_num * sizeof(int));
out.i.top = (int*)malloc(out.i.display_num * sizeof(int));
out.i.right = (int*)malloc(out.i.display_num * sizeof(int));
out.i.bottom = (int*)malloc(out.i.display_num * sizeof(int));
for (size_t idx = 0; idx < out.i.display_num; idx++) {
std::string name = displays[idx].at("name").get<std::string>();
out.i.display_list[idx] = (char*)malloc(name.size() + 1);
strcpy(out.i.display_list[idx], name.c_str());
out.i.left[idx] = displays[idx].at("left").get<int>();
out.i.top[idx] = displays[idx].at("top").get<int>();
out.i.right[idx] = displays[idx].at("right").get<int>();
out.i.bottom[idx] = displays[idx].at("bottom").get<int>();
}
break;
}
}
return true;
} catch (const std::exception& e) {
printf("Failed to parse RemoteAction JSON: %s\n", e.what());
return false;
}
}
static std::string ToJson(const RemoteAction& action);
static bool FromJson(const std::string& json_string, RemoteAction& output);
};
// Releases the dynamically allocated display arrays held by host information.
// Other RemoteAction variants do not own memory and are left unchanged.
void FreeRemoteAction(RemoteAction& action);
// int key_code, bool is_down, uint32_t scan_code, bool extended
typedef void (*OnKeyAction)(int, bool, uint32_t, bool, void*);
using OnKeyAction = void (*)(int, bool, uint32_t, bool, void*);
class DeviceController {
public:
virtual ~DeviceController() {}
public:
// virtual int Init(int screen_width, int screen_height);
// virtual int Destroy();
// virtual int SendMouseCommand(RemoteAction remote_action);
// virtual int Hook();
// virtual int Unhook();
virtual ~DeviceController() = default;
};
} // namespace crossdesk
#endif
@@ -13,6 +13,7 @@
#include <vector>
#include "device_controller.h"
#include "display_info.h"
struct DBusConnection;
struct DBusMessageIter;
@@ -4,6 +4,7 @@
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include "rd_log.h"
@@ -44,6 +45,13 @@ int MouseController::Init(std::vector<DisplayInfo> display_info_list) {
int MouseController::Destroy() { return 0; }
void MouseController::UpdateDisplayInfoList(
const std::vector<DisplayInfo>& display_info_list) {
if (!display_info_list.empty()) {
display_info_list_ = display_info_list;
}
}
int MouseController::BeginClick(ClickTracker& tracker, int x, int y) {
const auto now = std::chrono::steady_clock::now();
const bool continues_previous_click =
@@ -94,10 +102,26 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
return -1;
}
const float normalized_x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
const float normalized_y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
int mouse_pos_x = normalized_x * display_info.width + display_info.left;
int mouse_pos_y = normalized_y * display_info.height + display_info.top;
const double normalized_x =
std::clamp(static_cast<double>(remote_action.m.x), 0.0, 1.0);
const double normalized_y =
std::clamp(static_cast<double>(remote_action.m.y), 0.0, 1.0);
// Keep the coordinate continuous until it reaches Core Graphics. This
// avoids turning a one-pixel rounding error into a visible offset when the
// client zooms the remote image. The upper bound remains just inside the
// display because CG display rectangles use an exclusive right/bottom edge.
const double max_x = std::nextafter(static_cast<double>(display_info.right),
static_cast<double>(display_info.left));
const double max_y = std::nextafter(static_cast<double>(display_info.bottom),
static_cast<double>(display_info.top));
const double mouse_pos_x = std::clamp(
display_info.left + normalized_x * display_info.width,
static_cast<double>(display_info.left), max_x);
const double mouse_pos_y = std::clamp(
display_info.top + normalized_y * display_info.height,
static_cast<double>(display_info.top), max_y);
const int tracked_mouse_pos_x = static_cast<int>(std::lround(mouse_pos_x));
const int tracked_mouse_pos_y = static_cast<int>(std::lround(mouse_pos_y));
CGEventRef mouse_event = nullptr;
CGEventType mouse_type;
@@ -109,7 +133,8 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
case MouseFlag::left_down:
mouse_type = kCGEventLeftMouseDown;
left_dragging_ = true;
click_state = BeginClick(left_click_tracker_, mouse_pos_x, mouse_pos_y);
click_state = BeginClick(left_click_tracker_, tracked_mouse_pos_x,
tracked_mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonLeft);
SetClickState(mouse_event, click_state);
@@ -117,7 +142,8 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
case MouseFlag::left_up:
mouse_type = kCGEventLeftMouseUp;
left_dragging_ = false;
click_state = EndClick(left_click_tracker_, mouse_pos_x, mouse_pos_y);
click_state = EndClick(left_click_tracker_, tracked_mouse_pos_x,
tracked_mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonLeft);
SetClickState(mouse_event, click_state);
@@ -125,7 +151,8 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
case MouseFlag::right_down:
mouse_type = kCGEventRightMouseDown;
right_dragging_ = true;
click_state = BeginClick(right_click_tracker_, mouse_pos_x, mouse_pos_y);
click_state = BeginClick(right_click_tracker_, tracked_mouse_pos_x,
tracked_mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonRight);
SetClickState(mouse_event, click_state);
@@ -133,21 +160,24 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
case MouseFlag::right_up:
mouse_type = kCGEventRightMouseUp;
right_dragging_ = false;
click_state = EndClick(right_click_tracker_, mouse_pos_x, mouse_pos_y);
click_state = EndClick(right_click_tracker_, tracked_mouse_pos_x,
tracked_mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonRight);
SetClickState(mouse_event, click_state);
break;
case MouseFlag::middle_down:
mouse_type = kCGEventOtherMouseDown;
click_state = BeginClick(middle_click_tracker_, mouse_pos_x, mouse_pos_y);
click_state = BeginClick(middle_click_tracker_, tracked_mouse_pos_x,
tracked_mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonCenter);
SetClickState(mouse_event, click_state);
break;
case MouseFlag::middle_up:
mouse_type = kCGEventOtherMouseUp;
click_state = EndClick(middle_click_tracker_, mouse_pos_x, mouse_pos_y);
click_state = EndClick(middle_click_tracker_, tracked_mouse_pos_x,
tracked_mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonCenter);
SetClickState(mouse_event, click_state);
@@ -11,6 +11,7 @@
#include <vector>
#include "device_controller.h"
#include "display_info.h"
namespace crossdesk {
@@ -23,6 +24,8 @@ class MouseController : public DeviceController {
virtual int Init(std::vector<DisplayInfo> display_info_list);
virtual int Destroy();
virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
void UpdateDisplayInfoList(
const std::vector<DisplayInfo>& display_info_list);
private:
struct ClickTracker {
@@ -10,6 +10,7 @@
#include <vector>
#include "device_controller.h"
#include "display_info.h"
namespace crossdesk {
@@ -27,4 +28,4 @@ class MouseController : public DeviceController {
std::vector<DisplayInfo> display_info_list_;
};
} // namespace crossdesk
#endif
#endif
+300
View File
@@ -0,0 +1,300 @@
#include "device_controller.h"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <nlohmann/json.hpp>
namespace crossdesk {
namespace {
using json = nlohmann::json;
void ResetHostInfo(HostInfo& info) {
info.host_name[0] = '\0';
info.host_name_size = 0;
info.display_list = nullptr;
info.display_num = 0;
info.left = nullptr;
info.top = nullptr;
info.right = nullptr;
info.bottom = nullptr;
}
bool AllocateHostDisplays(HostInfo& info, std::size_t count) {
if (count == 0) return true;
info.display_list = static_cast<char**>(std::calloc(count, sizeof(char*)));
info.left = static_cast<int*>(std::malloc(count * sizeof(int)));
info.top = static_cast<int*>(std::malloc(count * sizeof(int)));
info.right = static_cast<int*>(std::malloc(count * sizeof(int)));
info.bottom = static_cast<int*>(std::malloc(count * sizeof(int)));
return info.display_list && info.left && info.top && info.right &&
info.bottom;
}
} // namespace
std::string RemoteAction::to_json() const { return ToJson(*this); }
bool RemoteAction::from_json(const std::string& json_string) {
RemoteAction temporary{};
if (!FromJson(json_string, temporary)) return false;
*this = temporary;
return true;
}
std::string RemoteAction::ToJson(const RemoteAction& action) {
if (action.type == ControlType::invalid) return {};
json object;
object["type"] = action.type;
switch (action.type) {
case ControlType::mouse:
object["mouse"] = {{"x", action.m.x},
{"y", action.m.y},
{"s", action.m.s},
{"flag", action.m.flag}};
break;
case ControlType::keyboard:
object["keyboard"] = {{"key_value", action.k.key_value},
{"scan_code", action.k.scan_code},
{"extended", action.k.extended},
{"flag", action.k.flag}};
break;
case ControlType::keyboard_state: {
json keys = json::array();
const std::size_t pressed_count =
std::min(action.ks.pressed_count, kMaxKeyboardStateKeys);
for (std::size_t index = 0; index < pressed_count; ++index) {
keys.push_back(
{{"key_value", action.ks.pressed_keys[index].key_value},
{"scan_code", action.ks.pressed_keys[index].scan_code},
{"extended", action.ks.pressed_keys[index].extended}});
}
object["keyboard_state"] =
{{"seq", action.ks.seq}, {"pressed_keys", keys}};
break;
}
case ControlType::cursor_state:
object["cursor_state"] =
{{"seq", action.cs.seq},
{"visible", action.cs.visible},
{"shape", static_cast<int>(action.cs.shape)},
{"position_valid", action.cs.position_valid},
{"x", action.cs.x},
{"y", action.cs.y},
{"visual_offset_x", action.cs.visual_offset_x},
{"visual_offset_y", action.cs.visual_offset_y},
{"display_id", action.cs.display_id},
{"position_update", action.cs.position_update}};
break;
case ControlType::audio_capture:
object["audio_capture"] = action.a;
break;
case ControlType::display_id:
object["display_id"] = action.d;
break;
case ControlType::service_status:
object["service_status"] =
{{"available", action.ss.available},
{"interactive_stage", action.ss.interactive_stage}};
break;
case ControlType::service_command:
object["service_command"] = {{"flag", action.c.flag}};
break;
case ControlType::host_infomation: {
json displays = json::array();
for (std::size_t index = 0; index < action.i.display_num; ++index) {
displays.push_back(
{{"name", action.i.display_list ? action.i.display_list[index]
: ""},
{"left", action.i.left ? action.i.left[index] : 0},
{"top", action.i.top ? action.i.top[index] : 0},
{"right", action.i.right ? action.i.right[index] : 0},
{"bottom", action.i.bottom ? action.i.bottom[index] : 0}});
}
object["host_info"] = {{"host_name", action.i.host_name},
{"display_num", action.i.display_num},
{"displays", displays}};
break;
}
case ControlType::invalid:
default:
return {};
}
return object.dump();
}
bool RemoteAction::FromJson(const std::string& json_string,
RemoteAction& output) {
bool owns_host_info = false;
try {
const json object = json::parse(json_string);
output.type = static_cast<ControlType>(object.at("type").get<int>());
switch (output.type) {
case ControlType::mouse:
output.m.x = object.at("mouse").at("x").get<float>();
output.m.y = object.at("mouse").at("y").get<float>();
output.m.s = object.at("mouse").at("s").get<int>();
output.m.flag = static_cast<MouseFlag>(
object.at("mouse").at("flag").get<int>());
break;
case ControlType::keyboard:
output.k.key_value =
object.at("keyboard").at("key_value").get<std::size_t>();
output.k.scan_code = object.at("keyboard").value("scan_code", 0u);
output.k.extended = object.at("keyboard").value("extended", false);
output.k.flag = static_cast<KeyFlag>(
object.at("keyboard").at("flag").get<int>());
break;
case ControlType::keyboard_state: {
const auto& keyboard_state_object = object.at("keyboard_state");
output.ks.seq = keyboard_state_object.value("seq", 0u);
output.ks.pressed_count = 0;
const auto keys =
keyboard_state_object.value("pressed_keys", json::array());
if (!keys.is_array()) break;
const std::size_t count =
std::min(keys.size(), kMaxKeyboardStateKeys);
for (std::size_t index = 0; index < count; ++index) {
output.ks.pressed_keys[index].key_value =
keys[index].at("key_value").get<std::size_t>();
output.ks.pressed_keys[index].scan_code =
keys[index].value("scan_code", 0u);
output.ks.pressed_keys[index].extended =
keys[index].value("extended", false);
}
output.ks.pressed_count = count;
break;
}
case ControlType::cursor_state: {
const auto& cursor_state_object = object.at("cursor_state");
const int shape = cursor_state_object.at("shape").get<int>();
if (shape < static_cast<int>(RemoteCursorShape::default_cursor) ||
shape > static_cast<int>(RemoteCursorShape::nwse_resize)) {
return false;
}
output.cs.seq = cursor_state_object.at("seq").get<uint32_t>();
output.cs.visible = cursor_state_object.at("visible").get<bool>();
output.cs.shape = static_cast<RemoteCursorShape>(shape);
output.cs.position_valid =
cursor_state_object.value("position_valid", false);
output.cs.x = cursor_state_object.value("x", 0.5f);
output.cs.y = cursor_state_object.value("y", 0.5f);
output.cs.visual_offset_x =
cursor_state_object.value("visual_offset_x", 0.0f);
output.cs.visual_offset_y =
cursor_state_object.value("visual_offset_y", 0.0f);
output.cs.display_id = cursor_state_object.value("display_id", -1);
// Cursor state messages predating position-only echo suppression
// always carried an authoritative position update.
output.cs.position_update =
cursor_state_object.value("position_update", true);
if (!std::isfinite(output.cs.x) || !std::isfinite(output.cs.y) ||
!std::isfinite(output.cs.visual_offset_x) ||
!std::isfinite(output.cs.visual_offset_y)) {
return false;
}
output.cs.x = std::clamp(output.cs.x, 0.0f, 1.0f);
output.cs.y = std::clamp(output.cs.y, 0.0f, 1.0f);
output.cs.visual_offset_x =
std::clamp(output.cs.visual_offset_x, -1.0f, 1.0f);
output.cs.visual_offset_y =
std::clamp(output.cs.visual_offset_y, -1.0f, 1.0f);
break;
}
case ControlType::audio_capture:
output.a = object.at("audio_capture").get<bool>();
break;
case ControlType::display_id:
output.d = object.at("display_id").get<int>();
break;
case ControlType::service_status: {
const auto& service_status_object = object.at("service_status");
output.ss.available = service_status_object.value("available", false);
const std::string stage = service_status_object.value(
"interactive_stage", std::string());
std::strncpy(output.ss.interactive_stage, stage.c_str(),
sizeof(output.ss.interactive_stage) - 1);
output.ss.interactive_stage[sizeof(output.ss.interactive_stage) - 1] =
'\0';
break;
}
case ControlType::service_command:
output.c.flag = static_cast<ServiceCommandFlag>(
object.at("service_command").at("flag").get<int>());
break;
case ControlType::host_infomation: {
ResetHostInfo(output.i);
owns_host_info = true;
const auto& host_info_object = object.at("host_info");
const std::string host_name =
host_info_object.at("host_name").get<std::string>();
std::strncpy(output.i.host_name, host_name.c_str(),
sizeof(output.i.host_name) - 1);
output.i.host_name[sizeof(output.i.host_name) - 1] = '\0';
output.i.host_name_size = std::strlen(output.i.host_name);
const auto& displays = host_info_object.at("displays");
if (!displays.is_array()) return false;
output.i.display_num =
host_info_object.at("display_num").get<std::size_t>();
if (output.i.display_num != displays.size()) return false;
if (!AllocateHostDisplays(output.i, output.i.display_num)) {
FreeRemoteAction(output);
return false;
}
for (std::size_t index = 0; index < output.i.display_num; ++index) {
const std::string name =
displays[index].at("name").get<std::string>();
output.i.display_list[index] =
static_cast<char*>(std::malloc(name.size() + 1));
if (!output.i.display_list[index]) {
FreeRemoteAction(output);
return false;
}
std::memcpy(output.i.display_list[index], name.c_str(),
name.size() + 1);
output.i.left[index] = displays[index].at("left").get<int>();
output.i.top[index] = displays[index].at("top").get<int>();
output.i.right[index] = displays[index].at("right").get<int>();
output.i.bottom[index] = displays[index].at("bottom").get<int>();
}
break;
}
default:
return false;
}
return true;
} catch (const std::exception& exception) {
if (owns_host_info) {
FreeRemoteAction(output);
}
std::fprintf(stderr, "Failed to parse RemoteAction JSON: %s\n",
exception.what());
return false;
}
}
void FreeRemoteAction(RemoteAction& action) {
if (action.type != ControlType::host_infomation) return;
if (action.i.display_list) {
for (std::size_t index = 0; index < action.i.display_num; ++index) {
std::free(action.i.display_list[index]);
}
}
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);
ResetHostInfo(action.i);
}
} // namespace crossdesk
+119 -36
View File
@@ -22,6 +22,7 @@
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -1807,60 +1808,142 @@ void GuiApplication::Tick() {
}
void GuiApplication::ShareLocalCursorState() {
constexpr auto kCursorEchoSuppressionInterval = 300ms;
constexpr auto kCursorStateHeartbeatInterval = 500ms;
constexpr auto kCursorPositionShareInterval = 16ms;
constexpr float kCursorPositionEpsilon = 0.00005f;
bool has_connected_controller = false;
std::vector<std::string> connected_controllers;
{
std::shared_lock lock(connection_status_mutex_);
has_connected_controller =
std::any_of(connection_status_.begin(), connection_status_.end(),
[](const auto& entry) {
return entry.second == ConnectionStatus::Connected;
});
connected_controllers.reserve(connection_status_.size());
for (const auto& [remote_id, status] : connection_status_) {
if (status == ConnectionStatus::Connected) {
connected_controllers.push_back(remote_id);
}
}
}
if (!is_server_mode_ || !peer_ || !has_connected_controller) {
has_shared_cursor_state_ = false;
last_cursor_state_share_time_ = {};
if (!is_server_mode_ || !peer_ || connected_controllers.empty()) {
cursor_delivery_states_.clear();
return;
}
CursorState sampled{};
if (!cursor_state_provider_.Sample(&sampled)) {
if (!cursor_state_provider_.Sample(devices_.display_info_list(),
selected_display_, &sampled)) {
return;
}
const auto now = std::chrono::steady_clock::now();
const bool changed =
!has_shared_cursor_state_ ||
sampled.visible != last_shared_cursor_state_.visible ||
sampled.shape != last_shared_cursor_state_.shape;
const bool heartbeat_due =
last_cursor_state_share_time_.time_since_epoch().count() == 0 ||
now - last_cursor_state_share_time_ >= kCursorStateHeartbeatInterval;
if (!changed && !heartbeat_due) {
return;
std::unordered_map<std::string, std::chrono::steady_clock::time_point>
last_input_times;
{
std::lock_guard lock(remote_pointer_input_mutex_);
for (const auto& remote_id : connected_controllers) {
const auto input_it = last_remote_pointer_input_time_.find(remote_id);
if (input_it != last_remote_pointer_input_time_.end()) {
last_input_times.emplace(remote_id, input_it->second);
}
}
}
std::unordered_set<std::string> connected_ids(connected_controllers.begin(),
connected_controllers.end());
std::erase_if(cursor_delivery_states_, [&](const auto& entry) {
return connected_ids.find(entry.first) == connected_ids.end();
});
struct CursorRecipient {
std::string remote_id;
bool include_position = true;
};
std::vector<CursorRecipient> recipients;
for (const auto& remote_id : connected_controllers) {
auto& delivery = cursor_delivery_states_[remote_id];
const auto input_it = last_input_times.find(remote_id);
const bool suppress_position =
input_it != last_input_times.end() &&
now - input_it->second < kCursorEchoSuppressionInterval;
delivery.feedback_pending =
delivery.feedback_pending || suppress_position;
const CursorState& previous = delivery.last_sent;
const bool shape_changed =
!delivery.has_sent || sampled.visible != previous.visible ||
sampled.shape != previous.shape ||
std::abs(sampled.visual_offset_x - previous.visual_offset_x) >
kCursorPositionEpsilon ||
std::abs(sampled.visual_offset_y - previous.visual_offset_y) >
kCursorPositionEpsilon;
// Preserve sub-point cursor motion. At 10x client zoom, the old roughly
// one-logical-pixel threshold became several visible phone points.
const bool position_changed =
!delivery.has_sent ||
sampled.position_valid != previous.position_valid ||
sampled.display_id != previous.display_id ||
(sampled.position_valid && previous.position_valid &&
(std::abs(sampled.x - previous.x) > kCursorPositionEpsilon ||
std::abs(sampled.y - previous.y) > kCursorPositionEpsilon));
const bool position_update_due =
position_changed &&
(delivery.last_sent_time.time_since_epoch().count() == 0 ||
now - delivery.last_sent_time >= kCursorPositionShareInterval);
const bool heartbeat_due =
delivery.last_sent_time.time_since_epoch().count() == 0 ||
now - delivery.last_sent_time >= kCursorStateHeartbeatInterval;
if (suppress_position) {
// Cursor appearance is independent of cursor position. Continue sending
// shape changes to every controller while withholding only the sampled
// position from the connection that originated recent input.
if (shape_changed || heartbeat_due) {
recipients.push_back({remote_id, false});
}
} else if (delivery.feedback_pending || shape_changed ||
position_update_due || heartbeat_due) {
recipients.push_back({remote_id, true});
}
}
if (recipients.empty()) return;
sampled.seq = ++cursor_state_sequence_;
RemoteAction action{};
action.type = ControlType::cursor_state;
action.cs = sampled;
const std::string message = action.to_json();
const int result = SendDataFrame(peer_, message.c_str(), message.size(),
mouse_label_.c_str());
if (result != 0) {
LOG_WARN("Send cursor state failed, ret={}", result);
return;
}
for (const auto& recipient : recipients) {
auto& delivery = cursor_delivery_states_[recipient.remote_id];
CursorState outgoing = sampled;
outgoing.position_update = recipient.include_position;
if (!recipient.include_position) {
// Keep legacy receivers at their last acknowledged position as well.
// New receivers honor position_update=false and leave their optimistic
// local position untouched.
outgoing.position_valid =
delivery.has_sent && delivery.last_sent.position_valid;
outgoing.x = delivery.has_sent ? delivery.last_sent.x : 0.5f;
outgoing.y = delivery.has_sent ? delivery.last_sent.y : 0.5f;
outgoing.display_id = delivery.has_sent
? delivery.last_sent.display_id
: -1;
}
if (changed) {
LOG_INFO("Sent cursor state: seq={}, visible={}, shape={}", sampled.seq,
sampled.visible, static_cast<int>(sampled.shape));
}
RemoteAction action{};
action.type = ControlType::cursor_state;
action.cs = outgoing;
const std::string message = action.to_json();
const int result = SendDataFrameToPeer(
peer_, message.c_str(), message.size(), mouse_label_.c_str(),
recipient.remote_id.c_str(), recipient.remote_id.size());
if (result != 0) {
LOG_WARN("Send cursor state to [{}] failed, ret={}",
recipient.remote_id, result);
continue;
}
last_shared_cursor_state_ = sampled;
has_shared_cursor_state_ = true;
last_cursor_state_share_time_ = now;
delivery.last_sent = outgoing;
delivery.has_sent = true;
if (recipient.include_position) {
delivery.feedback_pending = false;
}
delivery.last_sent_time = now;
}
}
void GuiApplication::HandlePasswordChangeResult() {
+12 -6
View File
@@ -1,9 +1,10 @@
#ifndef CROSSDESK_GUI_APPLICATION_H_
#define CROSSDESK_GUI_APPLICATION_H_
#ifndef _GUI_APPLICATION_H_
#define _GUI_APPLICATION_H_
#include <chrono>
#include <memory>
#include <string>
#include <unordered_map>
#include "runtime/cursor_state_provider.h"
#include "runtime/gui_runtime.h"
@@ -21,6 +22,12 @@ public:
private:
struct SlintUi;
struct CursorDeliveryState {
CursorState last_sent{};
bool has_sent = false;
bool feedback_pending = false;
std::chrono::steady_clock::time_point last_sent_time{};
};
void InitializeLogger();
void InitializeSettings();
@@ -73,10 +80,9 @@ private:
std::unique_ptr<SlintUi> ui_;
CursorStateProvider cursor_state_provider_;
CursorState last_shared_cursor_state_{};
bool has_shared_cursor_state_ = false;
std::unordered_map<std::string, CursorDeliveryState>
cursor_delivery_states_;
uint32_t cursor_state_sequence_ = 0;
std::chrono::steady_clock::time_point last_cursor_state_share_time_{};
std::chrono::steady_clock::time_point next_video_frame_time_{};
#if defined(__linux__) && !defined(__APPLE__)
bool use_xwayland_gui_ = false;
@@ -86,4 +92,4 @@ private:
} // namespace crossdesk
#endif // CROSSDESK_GUI_APPLICATION_H_
#endif
+9
View File
@@ -233,6 +233,15 @@ int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) {
int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
controlled_remote_id_ = "";
RemoteAction remote_action{};
if ((event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) &&
event.button.button != SDL_BUTTON_LEFT &&
event.button.button != SDL_BUTTON_RIGHT &&
event.button.button != SDL_BUTTON_MIDDLE) {
return 0;
}
float cursor_x = last_mouse_event.motion.x;
float cursor_y = last_mouse_event.motion.y;
@@ -461,7 +461,7 @@ void SessionDeviceManager::UpdateInteractions() {
owner_.mouse_controller_is_started_ = false;
}
#if defined(__linux__) && !defined(__APPLE__)
#if defined(__linux__) || defined(__APPLE__)
if (owner_.screen_capturer_is_started_ && screen_capturer_ &&
mouse_controller_) {
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
@@ -2,7 +2,6 @@
#include <algorithm>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <limits>
#include <memory>
@@ -236,15 +235,9 @@ void FileTransferManager::Unregister(uint32_t file_id, bool per_peer) {
}
void FileTransferManager::HandleAck(const char *data, size_t size) {
if (size < sizeof(FileTransferAck)) {
LOG_ERROR("FileTransferAck: buffer too small, size={}", size);
return;
}
FileTransferAck ack{};
std::memcpy(&ack, data, sizeof(ack));
if (ack.magic != kFileAckMagic) {
LOG_ERROR("FileTransferAck: invalid magic, got 0x{:08X}", ack.magic);
if (!protocol::DecodeFileTransferAck(data, size, &ack)) {
LOG_ERROR("FileTransferAck: invalid payload, size={}", size);
return;
}
+4
View File
@@ -123,6 +123,10 @@ void GuiRuntime::HandlePresenceProbeTimeout() {
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;
+66
View File
@@ -0,0 +1,66 @@
#ifndef _CURSOR_POSITION_H_
#define _CURSOR_POSITION_H_
#include <algorithm>
#include <vector>
#include "device_controller.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
+12 -2
View File
@@ -4,6 +4,8 @@
#include <windows.h>
#include "runtime/cursor_position.h"
namespace crossdesk {
namespace {
@@ -39,7 +41,8 @@ struct CursorStateProvider::Impl {};
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
CursorStateProvider::~CursorStateProvider() = default;
bool CursorStateProvider::Sample(CursorState* state) {
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state) {
if (!state) return false;
CURSORINFO info{};
@@ -50,6 +53,8 @@ bool CursorStateProvider::Sample(CursorState* state) {
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;
}
@@ -65,6 +70,7 @@ bool CursorStateProvider::Sample(CursorState* state) {
#include "linux_cursor_shape.h"
#include "platform.h"
#include "shared_cursor_state.h"
#include "runtime/cursor_position.h"
namespace crossdesk {
namespace {
@@ -91,8 +97,10 @@ struct CursorStateProvider::Impl {
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
CursorStateProvider::~CursorStateProvider() = default;
bool CursorStateProvider::Sample(CursorState* state) {
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state) {
if (!state || !impl_) return false;
ResetCursorPosition(state);
if (IsWaylandSession()) {
SharedCursorState shared{};
@@ -115,6 +123,8 @@ bool CursorStateProvider::Sample(CursorState* state) {
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;
}
+7 -4
View File
@@ -1,9 +1,11 @@
#ifndef CROSSDESK_GUI_CURSOR_STATE_PROVIDER_H_
#define CROSSDESK_GUI_CURSOR_STATE_PROVIDER_H_
#ifndef _CURSOR_STATE_PROVIDER_H_
#define _CURSOR_STATE_PROVIDER_H_
#include <memory>
#include <vector>
#include "device_controller.h"
#include "display_info.h"
namespace crossdesk {
@@ -17,7 +19,8 @@ class CursorStateProvider {
CursorStateProvider(const CursorStateProvider&) = delete;
CursorStateProvider& operator=(const CursorStateProvider&) = delete;
bool Sample(CursorState* state);
bool Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state);
private:
struct Impl;
@@ -26,4 +29,4 @@ class CursorStateProvider {
} // namespace crossdesk
#endif // CROSSDESK_GUI_CURSOR_STATE_PROVIDER_H_
#endif
+27 -1
View File
@@ -8,9 +8,17 @@
#include <cstdint>
#include <vector>
#include "runtime/cursor_position.h"
namespace crossdesk {
namespace {
// Quartz reports the arrow cursor's event hotspot. The visible apex in the
// current macOS system artwork is about 1.6 logical points above that hotspot
// (NSCursor.arrowCursor has a (5, 5) hotspot). Send this as presentation-only
// metadata so controllers can align their glyph without changing input.
constexpr double kDefaultArrowVisualTipYOffset = -1.6;
struct CursorFingerprint {
uint64_t pixel_hash = 0;
size_t width = 0;
@@ -144,7 +152,8 @@ struct CursorStateProvider::Impl {};
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
CursorStateProvider::~CursorStateProvider() = default;
bool CursorStateProvider::Sample(CursorState* state) {
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state) {
if (!state) return false;
#pragma clang diagnostic push
@@ -157,6 +166,23 @@ bool CursorStateProvider::Sample(CursorState* state) {
state->visible = visible && cursor != nil;
state->shape = state->visible ? ShapeFromMacCursor(cursor)
: RemoteCursorShape::none;
ResetCursorPosition(state);
CGEventRef event = CGEventCreate(nullptr);
if (event) {
const CGPoint location = CGEventGetLocation(event);
NormalizeCursorPosition(location.x, location.y, displays,
preferred_display, state);
if (state->position_valid && state->visible &&
state->shape == RemoteCursorShape::default_cursor &&
state->display_id >= 0 &&
state->display_id < static_cast<int>(displays.size())) {
const double display_height =
std::max(displays[state->display_id].height, 1);
state->visual_offset_y = static_cast<float>(
kDefaultArrowVisualTipYOffset / display_height);
}
CFRelease(event);
}
return true;
}
+3
View File
@@ -141,6 +141,9 @@ int GuiRuntime::CreateConnectionPeer() {
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
+20 -5
View File
@@ -97,6 +97,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
receiver.SetOnSendAck([runtime,
remote_user_id](const FileTransferAck &ack) -> int {
const auto encoded_ack = protocol::EncodeFileTransferAck(ack);
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
if (is_server_sending) {
auto props =
@@ -104,14 +105,14 @@ void PeerEventHandler::OnReceiveDataBuffer(
if (props) {
PeerPtr *peer = props->peer_;
return SendReliableDataFrame(
peer, reinterpret_cast<const char *>(&ack),
sizeof(FileTransferAck), runtime->file_feedback_label_.c_str());
peer, encoded_ack.data(), encoded_ack.size(),
runtime->file_feedback_label_.c_str());
}
}
return SendReliableDataFrame(
runtime->peer_, reinterpret_cast<const char *>(&ack),
sizeof(FileTransferAck), runtime->file_feedback_label_.c_str());
runtime->peer_, encoded_ack.data(), encoded_ack.size(),
runtime->file_feedback_label_.c_str());
});
receiver.OnData(data, size);
@@ -187,7 +188,15 @@ void PeerEventHandler::OnReceiveDataBuffer(
!props->remote_cursor_state_received_ ||
props->remote_cursor_state_.visible != remote_action.cs.visible ||
props->remote_cursor_state_.shape != remote_action.cs.shape;
props->remote_cursor_state_ = remote_action.cs;
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={}",
@@ -238,6 +247,12 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
} 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_) &&
+12 -11
View File
@@ -1,5 +1,5 @@
#ifndef CROSSDESK_GUI_REMOTE_SESSION_H_
#define CROSSDESK_GUI_REMOTE_SESSION_H_
#ifndef _REMOTE_SESSION_H_
#define _REMOTE_SESSION_H_
#include <atomic>
#include <chrono>
@@ -13,6 +13,7 @@
#include <string>
#include <vector>
#include "stream_names.h"
#include "device_controller.h"
#include "display_info.h"
#include "minirtc.h"
@@ -60,14 +61,14 @@ struct FileTransferState {
struct RemoteSession {
Params params_;
PeerPtr* peer_ = nullptr;
std::string audio_label_ = "control_audio";
std::string data_label_ = "data";
std::string mouse_label_ = "mouse";
std::string keyboard_label_ = "keyboard";
std::string file_label_ = "file";
std::string control_data_label_ = "control_data";
std::string file_feedback_label_ = "file_feedback";
std::string clipboard_label_ = "clipboard";
std::string audio_label_ = protocol::kAudioStream;
std::string data_label_ = protocol::kDataStream;
std::string mouse_label_ = protocol::kMouseStream;
std::string keyboard_label_ = protocol::kKeyboardStream;
std::string file_label_ = protocol::kFileStream;
std::string control_data_label_ = protocol::kControlStream;
std::string file_feedback_label_ = protocol::kFileFeedbackStream;
std::string clipboard_label_ = protocol::kClipboardStream;
std::string local_id_;
std::string remote_id_;
bool exit_ = false;
@@ -173,4 +174,4 @@ using RemoteSessionPtr = std::shared_ptr<RemoteSession>;
} // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_REMOTE_SESSION_H_
#endif
+16 -10
View File
@@ -1,5 +1,5 @@
#ifndef CROSSDESK_GUI_RUNTIME_STATE_H_
#define CROSSDESK_GUI_RUNTIME_STATE_H_
#ifndef _RUNTIME_STATE_H_
#define _RUNTIME_STATE_H_
#include <atomic>
#include <chrono>
@@ -57,14 +57,14 @@ struct PeerState {
std::string video_primary_label_ = "primary_display";
std::string video_secondary_label_ = "secondary_display";
std::string audio_label_ = "audio";
std::string data_label_ = "data";
std::string mouse_label_ = "mouse";
std::string keyboard_label_ = "keyboard";
std::string data_label_ = protocol::kDataStream;
std::string mouse_label_ = protocol::kMouseStream;
std::string keyboard_label_ = protocol::kKeyboardStream;
std::string info_label_ = "info";
std::string control_data_label_ = "control_data";
std::string file_label_ = "file";
std::string file_feedback_label_ = "file_feedback";
std::string clipboard_label_ = "clipboard";
std::string control_data_label_ = protocol::kControlStream;
std::string file_label_ = protocol::kFileStream;
std::string file_feedback_label_ = protocol::kFileFeedbackStream;
std::string clipboard_label_ = protocol::kClipboardStream;
Params params_;
};
@@ -180,6 +180,12 @@ struct ConnectionState {
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_;
@@ -202,4 +208,4 @@ struct RuntimeState : InfrastructureState,
} // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_RUNTIME_STATE_H_
#endif
+8 -63
View File
@@ -93,38 +93,8 @@ std::vector<char> FileSender::BuildChunk(uint32_t file_id, uint64_t offset,
uint32_t data_size,
const std::string* file_name,
bool is_first, bool is_last) {
FileChunkHeader header{};
header.magic = kFileChunkMagic;
header.file_id = file_id;
header.offset = offset;
header.total_size = total_size;
header.chunk_size = data_size;
header.name_len =
(file_name && is_first) ? static_cast<uint16_t>(file_name->size()) : 0;
header.flags = 0;
if (is_first) header.flags |= 0x01;
if (is_last) header.flags |= 0x02;
std::size_t total_size_bytes =
sizeof(FileChunkHeader) + header.name_len + header.chunk_size;
std::vector<char> buffer;
buffer.resize(total_size_bytes);
std::size_t offset_bytes = 0;
memcpy(buffer.data() + offset_bytes, &header, sizeof(FileChunkHeader));
offset_bytes += sizeof(FileChunkHeader);
if (header.name_len > 0 && file_name) {
memcpy(buffer.data() + offset_bytes, file_name->data(), header.name_len);
offset_bytes += header.name_len;
}
if (header.chunk_size > 0 && data) {
memcpy(buffer.data() + offset_bytes, data, header.chunk_size);
}
return buffer;
return protocol::EncodeFileChunk(file_id, offset, total_size, data,
data_size, file_name, is_first, is_last);
}
// ---------- FileReceiver ----------
@@ -167,40 +137,15 @@ std::filesystem::path FileReceiver::GetDefaultDesktopPath() {
}
bool FileReceiver::OnData(const char* data, size_t size) {
if (!data || size < sizeof(FileChunkHeader)) {
protocol::FileChunkView chunk;
if (!protocol::DecodeFileChunk(data, size, &chunk)) {
LOG_ERROR("FileReceiver::OnData: invalid buffer");
return false;
}
FileChunkHeader header{};
memcpy(&header, data, sizeof(FileChunkHeader));
if (header.magic != kFileChunkMagic) {
return false;
}
std::size_t header_and_name =
sizeof(FileChunkHeader) + static_cast<std::size_t>(header.name_len);
if (size < header_and_name ||
size < header_and_name + static_cast<std::size_t>(header.chunk_size)) {
LOG_ERROR("FileReceiver::OnData: buffer too small for header + payload");
return false;
}
const char* name_ptr = data + sizeof(FileChunkHeader);
std::string file_name;
const std::string* file_name_ptr = nullptr;
if (header.name_len > 0) {
file_name.assign(name_ptr,
name_ptr + static_cast<std::size_t>(header.name_len));
file_name_ptr = &file_name;
}
const char* payload = data + header_and_name;
std::size_t payload_size =
static_cast<std::size_t>(header.chunk_size); // may be 0
return HandleChunk(header, payload, payload_size, file_name_ptr);
const std::string* file_name =
chunk.file_name.empty() ? nullptr : &chunk.file_name;
return HandleChunk(chunk.header, chunk.payload, chunk.payload_size,
file_name);
}
bool FileReceiver::HandleChunk(const FileChunkHeader& header,
+6 -27
View File
@@ -7,7 +7,6 @@
#ifndef _FILE_TRANSFER_H_
#define _FILE_TRANSFER_H_
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <functional>
@@ -15,32 +14,11 @@
#include <unordered_map>
#include <vector>
#include "file_transfer_protocol.h"
#include "stream_names.h"
namespace crossdesk {
// Magic constants for file transfer protocol
constexpr uint32_t kFileChunkMagic = 0x4A4E544D; // 'JNTM'
constexpr uint32_t kFileAckMagic = 0x4A4E5443; // 'JNTC'
#pragma pack(push, 1)
struct FileChunkHeader {
uint32_t magic; // magic to identify file-transfer chunks
uint32_t file_id; // unique id per file transfer
uint64_t offset; // offset in file
uint64_t total_size; // total file size
uint32_t chunk_size; // payload size in this chunk
uint16_t name_len; // filename length (bytes), only set on first chunk
uint8_t flags; // bit0: is_first, bit1: is_last, others reserved
};
struct FileTransferAck {
uint32_t magic; // magic to identify file-transfer ack
uint32_t file_id; // must match FileChunkHeader.file_id
uint64_t acked_offset; // received offset
uint64_t total_size; // total file size
uint32_t flags; // bit0: completed, bit1: error
};
#pragma pack(pop)
class FileSender {
public:
using SendFunc = std::function<int(const char* data, size_t size)>;
@@ -58,7 +36,8 @@ class FileSender {
// `file_id` : file id to use (0 means auto-generate).
// Return 0 on success, <0 on error.
int SendFile(const std::filesystem::path& path, const std::string& label,
const SendFunc& send, std::size_t chunk_size = 64 * 1024,
const SendFunc& send,
std::size_t chunk_size = protocol::kFileChunkSize,
uint32_t file_id = 0);
// build a single encoded chunk buffer according to FileChunkHeader protocol.
@@ -119,4 +98,4 @@ class FileReceiver {
} // namespace crossdesk
#endif
#endif
+81
View File
@@ -0,0 +1,81 @@
#include "file_transfer_protocol.h"
#include <cstring>
#include <limits>
namespace crossdesk::protocol {
std::vector<char> EncodeFileChunk(uint32_t file_id, uint64_t offset,
uint64_t total_size, const char* data,
uint32_t data_size,
const std::string* file_name, bool is_first,
bool is_last) {
const std::size_t name_size = file_name && is_first ? file_name->size() : 0;
if (name_size > std::numeric_limits<uint16_t>::max() ||
offset > total_size || data_size > total_size - offset ||
(data_size > 0 && data == nullptr)) {
return {};
}
FileChunkHeader header{};
header.magic = kFileChunkMagic;
header.file_id = file_id;
header.offset = offset;
header.total_size = total_size;
header.chunk_size = data_size;
header.name_len = static_cast<uint16_t>(name_size);
header.flags = (is_first ? 0x01 : 0) | (is_last ? 0x02 : 0);
std::vector<char> output(sizeof(header) + name_size + data_size);
std::memcpy(output.data(), &header, sizeof(header));
std::size_t cursor = sizeof(header);
if (name_size > 0) {
std::memcpy(output.data() + cursor, file_name->data(), name_size);
cursor += name_size;
}
if (data_size > 0) {
std::memcpy(output.data() + cursor, data, data_size);
}
return output;
}
bool DecodeFileChunk(const char* data, std::size_t size,
FileChunkView* output) {
if (!data || !output || size < sizeof(FileChunkHeader)) return false;
FileChunkHeader header{};
std::memcpy(&header, data, sizeof(header));
if (header.magic != kFileChunkMagic) return false;
const std::size_t name_size = header.name_len;
const std::size_t payload_size = header.chunk_size;
if (name_size > size - sizeof(header)) return false;
const std::size_t payload_offset = sizeof(header) + name_size;
if (payload_size > size - payload_offset || header.offset > header.total_size ||
payload_size > header.total_size - header.offset) {
return false;
}
output->header = header;
output->file_name.assign(data + sizeof(header), name_size);
output->payload = data + payload_offset;
output->payload_size = payload_size;
return true;
}
std::array<char, sizeof(FileTransferAck)> EncodeFileTransferAck(
const FileTransferAck& ack) {
std::array<char, sizeof(FileTransferAck)> output{};
std::memcpy(output.data(), &ack, sizeof(ack));
return output;
}
bool DecodeFileTransferAck(const char* data, std::size_t size,
FileTransferAck* output) {
if (!data || !output || size != sizeof(FileTransferAck)) return false;
std::memcpy(output, data, sizeof(*output));
return output->magic == kFileAckMagic &&
output->acked_offset <= output->total_size;
}
} // namespace crossdesk::protocol
+67
View File
@@ -0,0 +1,67 @@
#ifndef _FILE_TRANSFER_PROTOCOL_H_
#define _FILE_TRANSFER_PROTOCOL_H_
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace crossdesk {
inline constexpr uint32_t kFileChunkMagic = 0x4A4E544D; // 'JNTM'
inline constexpr uint32_t kFileAckMagic = 0x4A4E5443; // 'JNTC'
#pragma pack(push, 1)
struct FileChunkHeader {
uint32_t magic;
uint32_t file_id;
uint64_t offset;
uint64_t total_size;
uint32_t chunk_size;
uint16_t name_len;
uint8_t flags;
};
struct FileTransferAck {
uint32_t magic;
uint32_t file_id;
uint64_t acked_offset;
uint64_t total_size;
uint32_t flags;
};
#pragma pack(pop)
static_assert(sizeof(FileChunkHeader) == 31,
"FileChunkHeader wire layout must remain stable");
static_assert(sizeof(FileTransferAck) == 28,
"FileTransferAck wire layout must remain stable");
namespace protocol {
struct FileChunkView {
FileChunkHeader header{};
std::string file_name;
const char* payload = nullptr;
std::size_t payload_size = 0;
};
std::vector<char> EncodeFileChunk(uint32_t file_id, uint64_t offset,
uint64_t total_size, const char* data,
uint32_t data_size,
const std::string* file_name, bool is_first,
bool is_last);
bool DecodeFileChunk(const char* data, std::size_t size,
FileChunkView* output);
std::array<char, sizeof(FileTransferAck)> EncodeFileTransferAck(
const FileTransferAck& ack);
bool DecodeFileTransferAck(const char* data, std::size_t size,
FileTransferAck* output);
} // namespace protocol
} // namespace crossdesk
#endif