Compare commits

..
7 Commits
18 changed files with 191 additions and 330 deletions
@@ -35,7 +35,6 @@
#include "platform/video_renderer.h"
#include "rendering/slint_video_presenter.h"
#include "runtime/privacy_control_state.h"
#include "runtime/mouse_input_trace.h"
#if _WIN32
#include <windows.h>
@@ -879,7 +878,6 @@ void DragWindow(WindowHandle& component, int phase, float mouse_x,
} // namespace
struct GuiApplication::SlintUi {
MouseTraceState unrouted_mouse_trace;
slint::ComponentHandle<ui::MainWindow> main = ui::MainWindow::create();
std::shared_ptr<slint::VectorModel<ui::ReleaseNoteBlock>>
release_note_blocks_model =
@@ -1739,22 +1737,6 @@ void GuiApplication::BindStreamCallbacks() {
CloseStreamTab(props->remote_id_);
}
});
stream->on_pointer_observed([this](slint::PointerEventButton button,
slint::PointerEventKind kind,
bool local_ui, bool dragging,
bool suppressed) {
auto props = SelectedSession();
auto& state = props ? props->mouse_ui_trace_ : ui_->unrouted_mouse_trace;
const auto ticket = state.Next();
if (ticket.sampled) {
LOG_INFO("Mouse UI event: peer=[{}] event={} mono_us={} button={} kind={} "
"local_ui={} dragging={} suppressed={} window_focus={}",
props ? props->remote_id_ : "none", ticket.event,
ticket.started_us, static_cast<int>(button),
static_cast<int>(kind), local_ui, dragging, suppressed,
focus_on_stream_window_);
}
});
stream->on_pointer_input([this](slint::PointerEventButton button,
slint::PointerEventKind kind, float x,
float y) {
@@ -2032,7 +2014,7 @@ void GuiApplication::ShareLocalCursorState() {
action.type = ControlType::cursor_state;
action.cs = outgoing;
const std::string message = action.to_json();
const int result = SendDataFrameToPeer(
const int result = SendReliableDataFrameToPeer(
peer_, message.c_str(), message.size(), mouse_label_.c_str(),
recipient.remote_id.c_str(), recipient.remote_id.size());
if (result != 0) {
@@ -2466,7 +2448,6 @@ void GuiApplication::SyncStreamWindow() {
(*ui_->stream)->set_stats_rows(ui_->stats_model);
BindStreamCallbacks();
(*ui_->stream)->show();
LOG_INFO("Mouse UI stream window shown");
// Repeat after the next layout pass because the backend resolves the
// initial DPI and preferred size asynchronously on some platforms.
ui_->stream_initial_position_attempts = 2;
@@ -2671,6 +2652,8 @@ void GuiApplication::SyncStreamWindow() {
(*ui_->stream)->set_selected_display(props->selected_display_);
const auto& net = props->net_traffic_stats_;
(*ui_->stream)->set_stats_encryption_enabled(
status == ConnectionStatus::Connected && net.srtp_active);
std::vector<ui::NetworkStatsRow> stats_rows;
stats_rows.reserve(4);
const auto append_stats_row = [&](const std::string& label,
@@ -3160,9 +3143,6 @@ void GuiApplication::SelectStreamTab(int index) {
}
const std::string selected_remote_id = ui_->tab_ids[index];
const bool selection_changed = focused_remote_id_ != selected_remote_id;
if (selection_changed) {
LOG_INFO("Mouse UI selected peer=[{}]", selected_remote_id);
}
bool renderer_selection_changed = false;
if (!controlled_remote_id_.empty() &&
controlled_remote_id_ != selected_remote_id) {
@@ -3267,12 +3247,7 @@ GuiApplication::SelectedSession() {
void GuiApplication::SendPointerInput(int button, int kind, float x, float y) {
auto props = SelectedSession();
MouseInputTrace trace(props ? &props->mouse_send_trace_ :
&ui_->unrouted_mouse_trace,
props ? props->remote_id_ : "none", "pointer");
if (!props || !props->peer_ || !props->control_mouse_ || !ui_->stream) {
trace.Result(!props ? "no_session" : !props->peer_ ? "no_peer" :
!props->control_mouse_ ? "control_disabled" : "no_window");
return;
}
@@ -3306,13 +3281,11 @@ void GuiApplication::SendPointerInput(int button, int kind, float x, float y) {
}
if (x < offset_x || x > offset_x + render_width || y < offset_y ||
y > offset_y + render_height) {
trace.Result("outside_video");
return;
}
RemoteAction action{};
action.type = ControlType::mouse;
trace.Result("unsupported_pointer_event");
action.m.x = std::clamp((x - offset_x) / render_width, 0.0f, 1.0f);
action.m.y = std::clamp((y - offset_y) / render_height, 0.0f, 1.0f);
// Slint enum order: Cancel=0, Down=1, Up=2, Move=3 and
@@ -3343,30 +3316,18 @@ void GuiApplication::SendPointerInput(int button, int kind, float x, float y) {
controlled_remote_id_ = props->remote_id_;
const std::string message = action.to_json();
trace.Result("pending");
trace.Payload(message.data(), message.size());
trace.Stage("cursor_state");
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->cursor_presentation_.NoteInput(SDL_GetTicks());
}
trace.Stage("send_api");
const int ret = SendDataFrame(props->peer_, message.c_str(), message.size(),
props->mouse_label_.c_str());
// This API may only acknowledge dispatch. The transport logs the actual
// ICE send result separately using the same payload tag.
trace.Result("send_api_returned", ret, ret != 0);
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
props->mouse_label_.c_str());
}
void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
float y) {
auto props = SelectedSession();
MouseInputTrace trace(props ? &props->mouse_send_trace_ :
&ui_->unrouted_mouse_trace,
props ? props->remote_id_ : "none", "scroll");
if (!props || !props->peer_ || !props->control_mouse_ || !ui_->stream) {
trace.Result(!props ? "no_session" : !props->peer_ ? "no_peer" :
!props->control_mouse_ ? "control_disabled" : "no_window");
return;
}
const auto size = (*ui_->stream)->window().size();
@@ -3381,7 +3342,6 @@ void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
(fullscreen_button_pressed_ ? 0.0f
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
if (width <= 0 || height <= 0) {
trace.Result("invalid_geometry");
return;
}
RemoteAction action{};
@@ -3396,7 +3356,6 @@ void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
action.m.s = delta_x > 0 ? 1 : delta_x < 0 ? -1 : 0;
}
if (action.m.s == 0) {
trace.Result("zero_scroll");
return;
}
{
@@ -3405,11 +3364,8 @@ void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
}
controlled_remote_id_ = props->remote_id_;
const std::string message = action.to_json();
trace.Payload(message.data(), message.size());
trace.Stage("send_api");
const int ret = SendDataFrame(props->peer_, message.c_str(), message.size(),
props->mouse_label_.c_str());
trace.Result("send_api_returned", ret, ret != 0);
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
props->mouse_label_.c_str());
}
void GuiApplication::SendKeyInput(const std::string& text, bool pressed,
@@ -366,8 +366,8 @@ int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
}
if (props->peer_) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->mouse_label_.c_str());
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->mouse_label_.c_str());
}
} else if (SDL_EVENT_MOUSE_WHEEL == event.type &&
last_mouse_event.button.x >= render_rect.x &&
@@ -412,8 +412,8 @@ int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
}
if (props->peer_) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->mouse_label_.c_str());
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->mouse_label_.c_str());
}
}
}
@@ -233,6 +233,9 @@ struct TranslationRow {
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
X(transport_encryption, u8"加密传输", "Encryption", u8"Шифрование") \
X(transport_encryption_enabled, u8"已开启", "Enabled", u8"Включено") \
X(transport_encryption_disabled, u8"未开启", "Disabled", u8"Выключено") \
X(online, u8"在线", "Online", u8"Онлайн") \
X(offline, u8"离线", "Offline", u8"Офлайн") \
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \
@@ -370,8 +370,6 @@ int SessionDeviceManager::StartMouseController() {
}
const int init_ret = mouse_controller_->Init(display_info_list_);
LOG_INFO("Mouse controller initialization: ret={}, displays={}", init_ret,
display_info_list_.size());
if (init_ret != 0) {
LOG_INFO("Destroy mouse controller");
mouse_controller_->Destroy();
@@ -634,12 +632,11 @@ bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
key_code, is_down, scan_code, extended) == 0;
}
int SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
void SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
int selected_display) {
if (mouse_controller_) {
return mouse_controller_->SendMouseCommand(action, selected_display);
mouse_controller_->SendMouseCommand(action, selected_display);
}
return -2;
}
int SessionDeviceManager::SwitchDisplay(int display_id) {
@@ -70,8 +70,7 @@ public:
bool SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code,
bool extended);
// -2 means the mouse controller has not been initialized.
int SendMouseCommand(const RemoteAction &action, int selected_display);
void SendMouseCommand(const RemoteAction &action, int selected_display);
int SwitchDisplay(int display_id);
void ResetToInitialDisplay();
@@ -518,7 +518,7 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
}
AddAudioStream(props->peer_, props->audio_label_.c_str());
AddDataStream(props->peer_, props->data_label_.c_str(), false);
AddDataStream(props->peer_, props->mouse_label_.c_str(), false);
AddDataStream(props->peer_, props->mouse_label_.c_str(), true);
AddDataStream(props->peer_, props->keyboard_label_.c_str(), true);
AddDataStream(props->peer_, props->control_data_label_.c_str(), true);
AddDataStream(props->peer_, props->file_label_.c_str(), true);
+1 -1
View File
@@ -199,7 +199,7 @@ int GuiRuntime::CreateConnectionPeer() {
AddAudioStream(peer_, audio_label_.c_str());
AddDataStream(peer_, data_label_.c_str(), false);
AddDataStream(peer_, mouse_label_.c_str(), false);
AddDataStream(peer_, mouse_label_.c_str(), true);
AddDataStream(peer_, keyboard_label_.c_str(), true);
AddDataStream(peer_, control_data_label_.c_str(), true);
AddDataStream(peer_, file_label_.c_str(), true);
@@ -1,88 +0,0 @@
#ifndef CROSSDESK_MOUSE_INPUT_TRACE_H_
#define CROSSDESK_MOUSE_INPUT_TRACE_H_
#include <minirtc_mouse_trace.h>
#include <string>
#include "rd_log.h"
namespace crossdesk {
using MouseTraceState = minirtc::diagnostics::MouseTraceState;
// Log entry before any potentially blocking check, and report each stage's
// duration. Steady-clock timestamps are comparable only within one process.
class MouseInputTrace {
public:
MouseInputTrace(MouseTraceState* state, const std::string& peer,
const char* scope, const void* data = nullptr,
size_t size = 0)
: state_(state), peer_(peer), scope_(scope) {
if (!state_) return;
ticket_ = state_->Next();
stage_started_us_ = ticket_.started_us;
if (data) tag_ = minirtc::diagnostics::MousePayloadTag(data, size);
Log("begin", 0, false);
stage_started_us_ = minirtc::diagnostics::MouseTraceNowUs();
}
MouseInputTrace(const MouseInputTrace&) = delete;
MouseInputTrace& operator=(const MouseInputTrace&) = delete;
~MouseInputTrace() {
if (state_)
Log("end", minirtc::diagnostics::MouseTraceNowUs() - stage_started_us_,
failed_);
}
void Stage(const char* stage) {
if (!state_) return;
const auto now = minirtc::diagnostics::MouseTraceNowUs();
Log("stage_done", now - stage_started_us_, false);
stage_ = stage;
stage_started_us_ = minirtc::diagnostics::MouseTraceNowUs();
Log("stage_begin", 0, false);
// Exclude synchronous logger flush time from the measured operation.
stage_started_us_ = minirtc::diagnostics::MouseTraceNowUs();
}
void Payload(const void* data, size_t size) {
tag_ = minirtc::diagnostics::MousePayloadTag(data, size);
}
void Result(const char* outcome, int result = 0, bool failed = false) {
outcome_ = outcome;
result_ = result;
failed_ = failed;
}
private:
void Log(const char* phase, int64_t stage_us, bool failed) {
const bool problem =
failed || stage_us >= minirtc::diagnostics::kSlowMouseTraceUs;
const bool warn = problem && state_->Problem();
if (!ticket_.sampled && !warn) return;
const auto now = minirtc::diagnostics::MouseTraceNowUs();
if (warn) {
LOG_WARN(
"Mouse trace: scope={} peer=[{}] event={} tag={:016x} phase={} "
"stage={} stage_us={} total_us={} mono_us={} outcome={} ret={}",
scope_, peer_, ticket_.event, tag_, phase, stage_, stage_us,
now - ticket_.started_us, now, outcome_, result_);
} else {
LOG_INFO(
"Mouse trace: scope={} peer=[{}] event={} tag={:016x} phase={} "
"stage={} stage_us={} total_us={} mono_us={} outcome={} ret={}",
scope_, peer_, ticket_.event, tag_, phase, stage_, stage_us,
now - ticket_.started_us, now, outcome_, result_);
}
}
MouseTraceState* state_;
std::string peer_;
const char* scope_;
const char* stage_ = "entry";
const char* outcome_ = "pending";
minirtc::diagnostics::MouseTraceTicket ticket_{};
int64_t stage_started_us_ = 0;
uint64_t tag_ = 0;
int result_ = 0;
bool failed_ = false;
};
} // namespace crossdesk
#endif
@@ -21,7 +21,6 @@
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#include "runtime/remote_action_codec.h"
#include "runtime/mouse_input_trace.h"
#if _WIN32
#include "interactive_state.h"
@@ -89,18 +88,6 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
std::string source_id = std::string(src_id, src_id_size);
std::shared_ptr<MouseTraceState> mouse_trace_state;
if (source_id == runtime->mouse_label_) {
std::lock_guard lock(handler->mouse_traces_mutex_);
auto& state = handler->mouse_traces_[std::string(user_id, user_id_size)];
if (!state) state = std::make_shared<MouseTraceState>();
mouse_trace_state = state;
}
// Before JSON parsing, authorization, or Windows session queries. An entry
// without a matching stage completion pinpoints a blocked callback.
MouseInputTrace mouse_trace(mouse_trace_state.get(),
std::string(user_id, user_id_size),
"receive", data, size);
if (source_id == runtime->file_label_) {
std::string remote_user_id = std::string(user_id, user_id_size);
@@ -154,9 +141,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
std::string json_str(data, size);
RemoteAction remote_action{};
mouse_trace.Stage("parse");
if (!remote_action.from_json(json_str)) {
mouse_trace.Result("invalid_payload", -1, true);
LOG_ERROR("Failed to parse RemoteAction JSON payload");
return;
}
@@ -189,12 +174,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
remote_action.type == ControlType::service_command || remote_action.type == ControlType::display_id ||
remote_action.type == ControlType::audio_capture;
if (operates_host) {
mouse_trace.Stage("authorize");
if (!runtime->IsAuthorizedController(remote_id)) {
mouse_trace.Result("unauthorized", -1, true);
return;
}
mouse_trace.Stage("desktop_check");
if (!runtime->IsAuthorizedController(remote_id)) return;
#ifdef _WIN32
// KeyboardController checks the desktop immediately before each native
// key injection, including state reconciliation and cleanup. Avoid a
@@ -206,7 +186,6 @@ void PeerEventHandler::OnReceiveDataBuffer(
runtime->privacy_.Fail("Secure desktop or lock screen; privacy screen will turn off");
}
#endif
mouse_trace.Stage("dispatch");
}
if (remote_action.type == ControlType::service_status) {
if (auto props = runtime->FindRemoteSession(remote_id)) {
@@ -345,7 +324,6 @@ void PeerEventHandler::OnReceiveDataBuffer(
runtime->selected_display_,
remote_action.m.x, remote_action.m.y,
&absolute_x, &absolute_y)) {
mouse_trace.Result("invalid_display", -1, true);
LOG_WARN("Secure desktop mouse injection skipped, invalid display "
"mapping: display_index={}, x={}, y={}",
runtime->selected_display_, remote_action.m.x,
@@ -353,14 +331,10 @@ void PeerEventHandler::OnReceiveDataBuffer(
return;
}
mouse_trace.Stage("secure_desktop_inject");
const std::string response = SendCrossDeskSecureDesktopMouseInput(
absolute_x, absolute_y, remote_action.m.s,
static_cast<int>(remote_action.m.flag), 1000);
auto json = nlohmann::json::parse(response, nullptr, false);
const bool submitted = !json.is_discarded() && json.value("ok", false);
mouse_trace.Result(submitted ? "submitted" : "secure_inject_failed",
submitted ? 0 : -1, !submitted);
if (json.is_discarded() || !json.value("ok", false)) {
LogSecureDesktopInputBlocked(
&runtime->last_local_secure_input_block_log_tick_, "local",
@@ -376,11 +350,8 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
#endif
if (remote_action.type == ControlType::mouse) {
mouse_trace.Stage("normal_desktop_inject");
const int ret = runtime->devices_.SendMouseCommand(
remote_action, runtime->selected_display_);
mouse_trace.Result(ret == -2 ? "no_controller" :
ret != 0 ? "inject_failed" : "submitted", ret, ret != 0);
runtime->devices_.SendMouseCommand(remote_action,
runtime->selected_display_);
} else if (remote_action.type == ControlType::audio_capture) {
if (remote_action.a)
runtime->devices_.StartSpeakerCapturer();
@@ -242,13 +242,6 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
std::string remote_id(user_id, user_id_size);
std::shared_ptr<GuiRuntime::RemoteSession> props;
LOG_INFO("Input connection status: peer=[{}], status={}", remote_id,
static_cast<int>(status));
if (status == ConnectionStatus::Disconnected ||
status == ConnectionStatus::Failed || status == ConnectionStatus::Closed) {
std::lock_guard lock(handler->mouse_traces_mutex_);
handler->mouse_traces_.erase(remote_id);
}
{
std::shared_lock lock(runtime->remote_sessions_mutex_);
auto it = runtime->remote_sessions_.find(remote_id);
@@ -9,10 +9,6 @@
#include <cstddef>
#include <string>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <minirtc_mouse_trace.h>
#include "minirtc.h"
#include "runtime/session_lifecycle.h"
@@ -58,12 +54,9 @@ private:
static void SendClientInfo(PeerPtr *peer, const std::string &client_id);
GuiRuntime &owner_;
std::mutex mouse_traces_mutex_;
std::unordered_map<std::string,
std::shared_ptr<minirtc::diagnostics::MouseTraceState>> mouse_traces_;
gui_detail::SessionCallbackGate callbacks_;
};
} // namespace crossdesk
#endif
#endif
@@ -24,7 +24,6 @@
#include "display_info.h"
#include "cursor_presentation.h"
#include "minirtc.h"
#include <minirtc_mouse_trace.h>
namespace crossdesk { class PeerEventHandler; }
@@ -69,8 +68,6 @@ struct FileTransferState {
// Runtime state for one connected or connecting remote endpoint. It groups the
// connection, media, input, window and transfer data that share one lifetime.
struct RemoteSession {
minirtc::diagnostics::MouseTraceState mouse_ui_trace_;
minirtc::diagnostics::MouseTraceState mouse_send_trace_;
Params params_;
PeerPtr* peer_ = nullptr;
std::shared_ptr<PeerEventHandler> peer_events_;
+143 -70
View File
@@ -45,6 +45,9 @@ export global StreamStrings {
in-out property <string> stats-loss-rate: "Loss Rate";
in-out property <string> stats-resolution: "Res";
in-out property <string> stats-connection-mode: "Mode";
in-out property <string> stats-encryption: "Encryption";
in-out property <string> stats-encryption-enabled: "Enabled";
in-out property <string> stats-encryption-disabled: "Disabled";
}
struct ControlBarTooltip {
@@ -166,6 +169,7 @@ export component StreamWindow inherits Window {
in property <string> stats-fps: "0";
in property <string> stats-resolution: "";
in property <string> stats-connection-mode: "";
in property <bool> stats-encryption-enabled: false;
in property <bool> file-transfer-visible: false;
in property <[FileTransferEntry]> file-transfers;
@@ -189,12 +193,17 @@ export component StreamWindow inherits Window {
callback toggle-maximize-stream-window;
callback close-stream-window;
callback pointer-input(PointerEventButton, PointerEventKind, length, length);
callback pointer-observed(PointerEventButton, PointerEventKind, bool, bool, bool);
callback scroll-input(length, length, length, length);
callback key-input(string, bool, bool, bool, bool, bool);
callback keyboard-focus-changed(bool);
private property <bool> control-expanded: true;
private property <length> control-toolbar-height: 38px;
private property <length> control-button-step: 32px;
private property <length> stats-row-height: 18px;
private property <length> stats-header-height: 18px;
private property <length> stats-content-height:
root.stats-header-height + root.stats-rows.length * root.stats-row-height + 44px;
private property <bool> display-menu-open: false;
private property <bool> display-menu-animation-enabled: false;
private property <float> display-menu-progress: 0.0;
@@ -284,7 +293,7 @@ export component StreamWindow inherits Window {
&& pointer-x <= display-menu-x + 180px
&& pointer-y >= display-menu-y
&& pointer-y <= display-menu-y + display-menu-height;
let shortcut-menu-x = control.x + (root.control-docked-left ? 41px : 74px);
let shortcut-menu-x = control.x + shortcut-button.x;
let shortcut-menu-y = control.y + 40px;
let over-shortcut-menu = root.shortcut-menu-open
&& pointer-x >= shortcut-menu-x
@@ -640,9 +649,6 @@ export component StreamWindow inherits Window {
root.suppress-next-remote-pointer-release
&& (event.kind == PointerEventKind.up
|| event.kind == PointerEventKind.cancel);
root.pointer-observed(event.button, event.kind,
over-local-ui, root.control-dragging,
suppress-remote-release);
if event.kind == PointerEventKind.down && !over-local-ui {
// Discard a stale marker if a popup consumed the
// whole previous gesture, including its release.
@@ -686,8 +692,10 @@ export component StreamWindow inherits Window {
control := Rectangle {
x: root.control-dragging ? root.control-drag-x : root.control-docked-left ? 0px : parent.width - self.width;
y: min(max(0px, root.control-pos-y), parent.height - self.height);
width: root.control-expanded ? min(332px, parent.width) : 20px;
height: root.stats-visible && root.control-expanded ? min(180px, parent.height) : 38px;
width: root.control-expanded ? min(76px + 8 * root.control-button-step, parent.width) : 20px;
height: root.stats-visible && root.control-expanded
? min(root.control-toolbar-height + root.stats-content-height + 8px, parent.height)
: root.control-toolbar-height;
background: white;
border-width: 1px;
border-color: ImGuiLineStyle.border;
@@ -723,7 +731,7 @@ export component StreamWindow inherits Window {
}
shortcut-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 41px : 74px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + root.control-button-step; y: 7px;
icon: FontAwesomeIcons.keyboard; tooltip: StreamStrings.send-shortcut;
clicked => {
display-menu-open-timer.running = false;
@@ -735,51 +743,51 @@ export component StreamWindow inherits Window {
}
mouse-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 73px : 106px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 2 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.computer-mouse; slashed: !root.mouse-control-enabled;
tooltip: root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse;
clicked => { root.toggle-mouse-control(); }
}
audio-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 137px : 170px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 4 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.volume-high; slashed: !root.audio-enabled;
tooltip: root.audio-enabled ? StreamStrings.mute : StreamStrings.audio;
clicked => { root.toggle-audio(); }
}
file-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 169px : 202px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 5 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file;
clicked => { root.select-file(); }
}
stats-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 201px : 234px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 6 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.signal; selected: root.stats-visible;
tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats;
clicked => { root.toggle-stats(); }
}
fullscreen-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 233px : 266px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 7 * root.control-button-step; y: 7px;
icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen;
clicked => { root.toggle-fullscreen(); }
}
close-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 265px : 298px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 8 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect;
clicked => { root.disconnect(); }
}
if root.control-expanded && root.control-docked-left: Rectangle {
x: 299px; y: 12px; width: 2px; height: 15px;
x: parent.width - 33px; y: 12px; width: 2px; height: 15px;
background: ImGuiLineStyle.control-separator;
}
collapse-button := ControlBarButton {
x: root.control-expanded ? (root.control-docked-left ? 307px : 10px) : 2px;
x: root.control-expanded ? (root.control-docked-left ? parent.width - 25px : 10px) : 2px;
y: 7px;
width: 15px;
icon: root.control-expanded
@@ -791,7 +799,7 @@ export component StreamWindow inherits Window {
privacy-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 105px : 138px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 3 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.user-secret;
slashed: root.privacy-off && !root.privacy-unsupported;
muted: root.privacy-unsupported;
@@ -803,75 +811,110 @@ export component StreamWindow inherits Window {
}
if root.stats-visible && root.control-expanded: stats-table := Rectangle {
x: 14px;
y: 38px;
width: parent.width - 28px;
height: 128px;
x: 8px;
y: root.control-toolbar-height + 2px;
width: parent.width - 16px;
height: root.stats-content-height;
background: transparent;
private property <length> row-height: 16px;
private property <length> label-column-width: 60px;
private property <length> loss-column-width: 66px;
private property <length> row-height: root.stats-row-height;
private property <length> label-column-width: 50px;
private property <length> loss-column-width:
max((self.width - self.label-column-width) / 3,
stats-loss-label.preferred-width + 8px);
private property <length> value-column-width:
(self.width - self.label-column-width - self.loss-column-width) / 2;
private property <length> details-right-width:
right-label-width-probe.preferred-width + 6px
+ max(resolution-width-probe.preferred-width,
encryption-width-probe.preferred-width
+ (root.stats-encryption-enabled ? 14px : 0px));
private property <length> details-right-x: self.width / 2;
Rectangle {
y: 0px;
height: 1px;
background: ImGuiLineStyle.border;
left-label-width-probe := Text {
visible: false;
text: "FPS\n" + StreamStrings.stats-connection-mode;
font-size: ImGuiFontStyle.small;
}
right-label-width-probe := Text {
visible: false;
text: StreamStrings.stats-resolution + "\n" + StreamStrings.stats-encryption;
font-size: ImGuiFontStyle.small;
}
resolution-width-probe := Text {
visible: false;
text: root.stats-resolution;
font-size: ImGuiFontStyle.body;
font-weight: 600;
}
encryption-width-probe := Text {
visible: false;
text: root.stats-encryption-enabled
? StreamStrings.stats-encryption-enabled : StreamStrings.stats-encryption-disabled;
font-size: ImGuiFontStyle.body;
font-weight: 600;
}
Rectangle {
x: 0px;
y: 0px;
height: stats-table.row-height;
width: parent.width;
height: root.stats-header-height;
background: #f3f5f8;
border-radius: 4px;
Text {
x: stats-table.label-column-width + 4px;
width: stats-table.value-column-width - 8px;
text: StreamStrings.stats-in;
color: #30343b;
color: #667085;
font-size: ImGuiFontStyle.small;
horizontal-alignment: right;
vertical-alignment: center;
}
Text {
x: stats-table.label-column-width + stats-table.value-column-width + 4px;
width: stats-table.value-column-width - 8px;
text: StreamStrings.stats-out;
color: #30343b;
color: #667085;
font-size: ImGuiFontStyle.small;
horizontal-alignment: right;
vertical-alignment: center;
}
Text {
stats-loss-label := Text {
x: stats-table.label-column-width + stats-table.value-column-width * 2 + 4px;
width: stats-table.loss-column-width - 8px;
text: StreamStrings.stats-loss-rate;
color: #30343b;
color: #667085;
font-size: ImGuiFontStyle.small;
horizontal-alignment: right;
vertical-alignment: center;
overflow: elide;
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: ImGuiLineStyle.border;
}
}
for row[index] in root.stats-rows: Rectangle {
y: (index + 1) * stats-table.row-height;
x: 0px;
y: root.stats-header-height + index * stats-table.row-height;
width: parent.width;
height: stats-table.row-height;
background: index == root.stats-rows.length - 1 ? #edf3fc : transparent;
border-radius: 4px;
Text {
x: 4px;
width: stats-table.label-column-width - 8px;
text: row.label;
color: #30343b;
font-size: ImGuiFontStyle.small;
color: #475467;
font-size: ImGuiFontStyle.body;
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
vertical-alignment: center;
}
Text {
x: stats-table.label-column-width + 4px;
width: stats-table.value-column-width - 8px;
text: row.inbound;
color: #30343b;
font-size: ImGuiFontStyle.small;
color: #253247;
font-size: ImGuiFontStyle.body;
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
horizontal-alignment: right;
vertical-alignment: center;
overflow: elide;
}
@@ -879,8 +922,10 @@ export component StreamWindow inherits Window {
x: stats-table.label-column-width + stats-table.value-column-width + 4px;
width: stats-table.value-column-width - 8px;
text: row.outbound;
color: #30343b;
font-size: ImGuiFontStyle.small;
color: #253247;
font-size: ImGuiFontStyle.body;
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
horizontal-alignment: right;
vertical-alignment: center;
overflow: elide;
}
@@ -888,46 +933,73 @@ export component StreamWindow inherits Window {
x: stats-table.label-column-width + stats-table.value-column-width * 2 + 4px;
width: stats-table.loss-column-width - 8px;
text: row.loss-rate;
color: #30343b;
font-size: ImGuiFontStyle.small;
color: #475467;
font-size: ImGuiFontStyle.body;
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
horizontal-alignment: right;
vertical-alignment: center;
overflow: elide;
}
Rectangle {
if index < root.stats-rows.length - 2: Rectangle {
x: 4px;
y: parent.height - 1px;
width: parent.width - 8px;
height: 1px;
background: ImGuiLineStyle.border;
background: #edf0f4;
}
}
for metric[index] in [
{ label: "FPS:", value: root.stats-fps },
{ label: StreamStrings.stats-resolution + ":", value: root.stats-resolution },
{ label: StreamStrings.stats-connection-mode + ":", value: root.stats-connection-mode },
{ label: "FPS", value: root.stats-fps },
{ label: StreamStrings.stats-resolution, value: root.stats-resolution },
{ label: StreamStrings.stats-connection-mode, value: root.stats-connection-mode },
{ label: StreamStrings.stats-encryption, value: root.stats-encryption-enabled
? StreamStrings.stats-encryption-enabled : StreamStrings.stats-encryption-disabled },
]: Rectangle {
y: (index + 5) * stats-table.row-height;
height: stats-table.row-height;
Text {
x: 4px;
width: stats-table.label-column-width - 8px;
private property <length> encryption-icon-width:
index == 3 && root.stats-encryption-enabled ? 14px : 0px;
x: mod(index, 2) == 1 ? stats-table.details-right-x : 6px;
y: root.stats-header-height + root.stats-rows.length * stats-table.row-height
+ 2px + floor(index / 2) * 21px;
width: mod(index, 2) == 1 ? stats-table.details-right-width
: stats-table.details-right-x - 14px;
height: 21px;
metric-label := Text {
x: 0px;
y: 0px;
width: mod(index, 2) == 1 ? right-label-width-probe.preferred-width
: left-label-width-probe.preferred-width;
height: parent.height;
text: metric.label;
color: #30343b;
color: #667085;
font-size: ImGuiFontStyle.small;
vertical-alignment: center;
}
Text {
x: stats-table.label-column-width + 4px;
width: stats-table.value-column-width - 8px;
metric-value := Text {
x: metric-label.width + 6px;
y: 0px;
height: parent.height;
width: parent.width - self.x - parent.encryption-icon-width;
text: metric.value;
color: #30343b;
font-size: ImGuiFontStyle.small;
color: index == 3 && root.stats-encryption-enabled ? #18794e : #253247;
font-size: ImGuiFontStyle.body;
font-weight: 600;
horizontal-alignment: left;
vertical-alignment: center;
overflow: elide;
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: ImGuiLineStyle.border;
if index == 3 && root.stats-encryption-enabled: Text {
x: metric-value.x + min(metric-value.preferred-width, metric-value.width) + 4px;
y: metric-value.y + (metric-value.height - self.height) / 2;
width: 10px;
height: 12px;
text: FontAwesomeIcons.shield-halved;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 10px;
color: #238653;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
@@ -1011,7 +1083,7 @@ export component StreamWindow inherits Window {
}
if root.shortcut-menu-open: Rectangle {
x: control.x + (root.control-docked-left ? 41px : 74px); y: control.y + 40px; width: 180px; height: 68px;
x: control.x + shortcut-button.x; y: control.y + 40px; width: 180px; height: 68px;
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 5px; z: 11;
VerticalLayout {
padding: 4px; spacing: 1px;
@@ -1043,8 +1115,9 @@ export component StreamWindow inherits Window {
x: min(max(4px, control.x + root.hovered-control.anchor-x - self.width / 2),
video.width - self.width - 4px);
height: control-tip.preferred-height + 12px;
y: control.y + control.height + self.height + 8px <= video.height
? control.y + control.height + 4px : max(4px, control.y - self.height - 4px);
// Anchor to the button row so expanded stats stay beneath the tooltip.
y: control.y + root.control-toolbar-height + self.height + 8px <= video.height
? control.y + root.control-toolbar-height + 4px : max(4px, control.y - self.height - 4px);
background: #30343bf5; border-radius: 5px; z: 12;
control-tip := Text {
x: 8px; y: 6px; width: parent.width - 16px;
@@ -166,6 +166,12 @@ inline int ApplyStreamWindowStrings(
strings.set_stats_resolution(Text(localization::resolution[language]));
strings.set_stats_connection_mode(
Text(localization::connection_mode[language]));
strings.set_stats_encryption(
Text(localization::transport_encryption[language]));
strings.set_stats_encryption_enabled(
Text(localization::transport_encryption_enabled[language]));
strings.set_stats_encryption_disabled(
Text(localization::transport_encryption_disabled[language]));
return language;
}
@@ -621,21 +621,15 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
y:(float)y
action:(CrossDeskPointerAction)action {
const std::string message = MouseJSON(x, y, 0, static_cast<int>(action));
// Movement is latency-sensitive and can be superseded by newer positions.
// Button transitions must not be lost or reordered, otherwise a tap can be
// applied at a stale pointer position or leave a button logically pressed.
// MiniRTC reliability is configured per stream, so transitions use the
// existing reliable control stream while movement stays on the mouse stream.
const BOOL is_movement = action == CrossDeskPointerActionMove;
// Keep movement, button transitions, and scrolling ordered on the reliable
// mouse stream.
#if DEBUG
if (!is_movement) {
if (action != CrossDeskPointerActionMove) {
NSLog(@"CrossDesk pointer action=%ld normalized=(%.5f, %.5f)",
static_cast<long>(action), x, y);
}
#endif
[self sendMessage:message
reliable:!is_movement
stream:is_movement ? kMouseStream : kControlStream];
[self sendMessage:message reliable:YES stream:kMouseStream];
}
- (void)sendScrollX:(float)x
@@ -648,7 +642,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
const int flag = vertical ? 7 : 8;
const int wheel = raw_delta > 0 ? 1 : -1;
const std::string message = MouseJSON(x, y, wheel, flag);
[self sendMessage:message reliable:NO stream:kMouseStream];
[self sendMessage:message reliable:YES stream:kMouseStream];
}
- (void)sendWindowsKeyCode:(NSUInteger)keyCode isDown:(BOOL)isDown {
@@ -893,7 +887,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
// display. Remote display receivers are created from the host's SDP.
AddAudioStream(_state->controller_peer, kAudioStream);
AddDataStream(_state->controller_peer, kDataStream, false);
AddDataStream(_state->controller_peer, kMouseStream, false);
AddDataStream(_state->controller_peer, kMouseStream, true);
AddDataStream(_state->controller_peer, kKeyboardStream, true);
AddDataStream(_state->controller_peer, kControlStream, true);
AddDataStream(_state->controller_peer, kFileStream, true);
-45
View File
@@ -1,45 +0,0 @@
# 鼠标输入启动诊断
控制端和被控端都需要重新编译 CrossDesk 及 `deps/submodules/minirtc`。
诊断默认使用 INFO/WARN 日志。连接后立即在远端画面内持续移动鼠标,
保留两端同一次运行的 `crossdesk-*.log` 和 `minirtc-*.log`。
## 记录顺序
| 日志 | 所在端 | 含义 |
| --- | --- | --- |
| `Input connection status` | 两端 CrossDesk | 上层连接状态变化 |
| `Mouse UI stream window shown` / `Mouse UI selected` | 控制端 CrossDesk | 窗口显示和当前会话选择 |
| `Mouse UI event` | 控制端 CrossDesk | Slint 视频输入区域收到事件,包含本地 UI 遮挡、拖动和释放抑制状态 |
| `Mouse trace: scope=pointer` / `scope=scroll` | 控制端 CrossDesk | 转发入口、过滤原因、调用发送 API 的时间和返回值 |
| `Mouse transport dispatch` | 控制端 MiniRTC | 连接与 ICE 就绪检查及传输层返回值 |
| `Mouse RTP enqueue` | 控制端 MiniRTC | 实际入队结果,队列满时明确记录 |
| `Mouse RTP send begin` / `send end` | 控制端 MiniRTC | 队列等待、底层发送调用耗时及真实返回值 |
| `Mouse RTP receive` | 被控端 MiniRTC | RTP 包进入鼠标数据通道,位于应用回调之前 |
| `Mouse trace: scope=receive` | 被控端 CrossDesk | 应用回调入口及各检查、提交阶段 |
| `Mouse RTP callback complete` | 被控端 MiniRTC | 应用回调的总耗时 |
应用接收阶段依次为 `parse`、`authorize`、`desktop_check`、`dispatch`,
随后是 `normal_desktop_inject` 或 `secure_desktop_inject`。
`phase=stage_begin` 在调用前记录,`phase=stage_done` 或 `phase=end`
报告该阶段耗时。若某阶段开始后长时间没有结束,可以确定阻塞区间。
`outcome=no_controller`、`unauthorized`、`inject_failed` 等说明具体处理结果;
`submitted` 表示操作系统提交调用成功,不是目标应用已执行的确认。
## 关联与限频
- `tag` 是鼠标消息原始字节的 FNV-1a 64 位摘要,可关联发送和接收;相同消息
会有相同摘要,因此重复消息应同时结合 RTP 的 `ssrc` / `seq` 和时间判断。
- `event` 是各阶段本地计数,不应视为跨端共同序号。会话/通道重建后重新计数。
- `mono_us` 是进程所在机器的单调时钟,只用于该端的耗时判断。
两端日志的墙钟可能不同,不能直接相减当作网络时延。
- `queue_us`、`send_us`、`stage_us`、`dispatch_us` 和 `total_us` 单位均为微秒。
`stage_us` 和 `send_us` 排除本阶段入口日志写入时间;总耗时仍包含诊断开销。
- 每个采样器记录前 3 次事件,此后最多每 5 秒选取一次事件。
失败和耗时达到 100 毫秒的调用使用独立限频,前 3 次记录,此后每 5 秒一次。
- 发送 API 的返回值不等于网络送达确认,应检查传输层、RTP 发送和远端接收记录。
如果有 UI 事件而没有发送入口,先看 `local_ui`、`dragging`、`suppressed`;
如果已入队却迟迟没有实际发送,查看 `queue_us`;如果远端已有 RTP 记录,
但应用处理耗时很长,查看接收阶段。仅当两端相关阶段都及时完成时,
才进一步检查 Windows 注入后的钩子或目标应用行为。
+12
View File
@@ -0,0 +1,12 @@
# **更新内容**
1. 新增隐私屏,可隐藏被控电脑的本地画面并屏蔽键鼠,断开连接后自动恢复。
2. 提升远程直连成功率,改善网络波动时的连接稳定性。
3. Windows新增屏幕采集方式选择,方便适配不同设备。
4. 修复Windows锁屏、隐私模式等场景下光标消失、错位或重复的问题。
5. 提升远程画面缩小时的清晰度,改善画面流畅度。
6. 修复开启或关闭远程声音时可能卡住的问题。
7. 改进远程键鼠操作,修复Linux X11键盘输入异常和macOS窗口缩放光标显示问题。
8. 优化控制栏提示、网络状态和程序图标显示,新增连接加密状态。
PC客户端版本为v1.5.1。隐私屏支持Windows、macOS 14及以上版本和Linux X11,需连接双方均支持,暂不支持Web及旧版客户端。