mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-08-31 13:32:10 +08:00
[feat] add native iOS client support
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_) &&
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user