mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-29 17:00:48 +08:00
fix(rustdesk): reduce mouse input latency
This commit is contained in:
@@ -21,11 +21,13 @@ use crate::video::codec::BitratePreset;
|
|||||||
use crate::video::codec_constraints::{encoder_codec_to_id, encoder_codec_to_video_codec};
|
use crate::video::codec_constraints::{encoder_codec_to_id, encoder_codec_to_video_codec};
|
||||||
use crate::video::stream_manager::VideoStreamManager;
|
use crate::video::stream_manager::VideoStreamManager;
|
||||||
|
|
||||||
use super::bytes_codec::{read_frame, write_frame, write_frame_buffered};
|
use super::bytes_codec::{read_frame, write_frame_buffered};
|
||||||
use super::config::RustDeskConfig;
|
use super::config::RustDeskConfig;
|
||||||
use super::crypto::{self, KeyPair, SigningKeyPair};
|
use super::crypto::{self, KeyPair, SigningKeyPair};
|
||||||
use super::frame_adapters::{AudioFrameAdapter, VideoCodec, VideoFrameAdapter};
|
use super::frame_adapters::{AudioFrameAdapter, VideoCodec, VideoFrameAdapter};
|
||||||
use super::hid_adapter::{convert_key_events, convert_mouse_event, mouse_type};
|
use super::hid_adapter::{
|
||||||
|
convert_key_events, convert_mouse_event, convert_relative_mouse_delta, mouse_type,
|
||||||
|
};
|
||||||
use super::protocol::{
|
use super::protocol::{
|
||||||
decode_message, login_response, message, misc, Clipboard, ControlKey, DisplayInfo, Hash,
|
decode_message, login_response, message, misc, Clipboard, ControlKey, DisplayInfo, Hash,
|
||||||
HbbMessage, IdPk, KeyEvent, LoginRequest, LoginResponse, Misc, MouseEvent, OptionMessage,
|
HbbMessage, IdPk, KeyEvent, LoginRequest, LoginResponse, Misc, MouseEvent, OptionMessage,
|
||||||
@@ -41,6 +43,10 @@ const DEFAULT_SCREEN_HEIGHT: u32 = 1080;
|
|||||||
/// Default mouse event throttle interval (16ms ≈ 60Hz)
|
/// Default mouse event throttle interval (16ms ≈ 60Hz)
|
||||||
const DEFAULT_MOUSE_THROTTLE_MS: u64 = 16;
|
const DEFAULT_MOUSE_THROTTLE_MS: u64 = 16;
|
||||||
|
|
||||||
|
/// Bound relative movement accumulated during throttling. This prevents a malformed
|
||||||
|
/// client from turning one input event into an excessively large HID burst.
|
||||||
|
const MAX_PENDING_RELATIVE_DELTA: i64 = 4096;
|
||||||
|
|
||||||
/// Advertised RustDesk version for client compatibility.
|
/// Advertised RustDesk version for client compatibility.
|
||||||
const RUSTDESK_COMPAT_VERSION: &str = "1.4.5";
|
const RUSTDESK_COMPAT_VERSION: &str = "1.4.5";
|
||||||
// Advertised platform for RustDesk clients. This affects which UI options are shown.
|
// Advertised platform for RustDesk clients. This affects which UI options are shown.
|
||||||
@@ -56,6 +62,10 @@ struct InputThrottler {
|
|||||||
last_mouse_time: Instant,
|
last_mouse_time: Instant,
|
||||||
/// Minimum interval between mouse move events
|
/// Minimum interval between mouse move events
|
||||||
mouse_interval: Duration,
|
mouse_interval: Duration,
|
||||||
|
/// Relative mouse deltas cannot be dropped: unlike absolute coordinates, a later
|
||||||
|
/// event cannot recover movement lost during throttling.
|
||||||
|
pending_relative_x: i64,
|
||||||
|
pending_relative_y: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputThrottler {
|
impl InputThrottler {
|
||||||
@@ -64,11 +74,12 @@ impl InputThrottler {
|
|||||||
Self {
|
Self {
|
||||||
last_mouse_time: Instant::now() - Duration::from_millis(DEFAULT_MOUSE_THROTTLE_MS),
|
last_mouse_time: Instant::now() - Duration::from_millis(DEFAULT_MOUSE_THROTTLE_MS),
|
||||||
mouse_interval: Duration::from_millis(DEFAULT_MOUSE_THROTTLE_MS),
|
mouse_interval: Duration::from_millis(DEFAULT_MOUSE_THROTTLE_MS),
|
||||||
|
pending_relative_x: 0,
|
||||||
|
pending_relative_y: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a mouse move event should be sent
|
/// Check if a mouse move event should be sent.
|
||||||
/// Returns true if enough time has passed since the last event
|
|
||||||
fn should_send_mouse_move(&mut self) -> bool {
|
fn should_send_mouse_move(&mut self) -> bool {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
if now.duration_since(self.last_mouse_time) >= self.mouse_interval {
|
if now.duration_since(self.last_mouse_time) >= self.mouse_interval {
|
||||||
@@ -79,12 +90,91 @@ impl InputThrottler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn accumulate_relative_move(&mut self, x: i32, y: i32) {
|
||||||
|
self.pending_relative_x = (self.pending_relative_x + i64::from(x))
|
||||||
|
.clamp(-MAX_PENDING_RELATIVE_DELTA, MAX_PENDING_RELATIVE_DELTA);
|
||||||
|
self.pending_relative_y = (self.pending_relative_y + i64::from(y))
|
||||||
|
.clamp(-MAX_PENDING_RELATIVE_DELTA, MAX_PENDING_RELATIVE_DELTA);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_relative_move(&mut self) -> Option<(i32, i32)> {
|
||||||
|
if self.pending_relative_x == 0 && self.pending_relative_y == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let movement = (
|
||||||
|
self.pending_relative_x as i32,
|
||||||
|
self.pending_relative_y as i32,
|
||||||
|
);
|
||||||
|
self.pending_relative_x = 0;
|
||||||
|
self.pending_relative_y = 0;
|
||||||
|
Some(movement)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_relative_move(&mut self) {
|
||||||
|
self.pending_relative_x = 0;
|
||||||
|
self.pending_relative_y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// Force update the last mouse time (for button events that must be sent)
|
/// Force update the last mouse time (for button events that must be sent)
|
||||||
fn mark_mouse_sent(&mut self) {
|
fn mark_mouse_sent(&mut self) {
|
||||||
self.last_mouse_time = Instant::now();
|
self.last_mouse_time = Instant::now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serializes all outbound RustDesk messages while allowing the connection reader to
|
||||||
|
/// keep processing input when video or audio writes are backpressured.
|
||||||
|
struct OutboundWriter {
|
||||||
|
state: Mutex<OutboundWriterState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OutboundWriterState {
|
||||||
|
writer: OwnedWriteHalf,
|
||||||
|
session_key: Option<secretbox::Key>,
|
||||||
|
enc_seqnum: u64,
|
||||||
|
frame_buf: BytesMut,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutboundWriter {
|
||||||
|
fn new(writer: OwnedWriteHalf) -> Self {
|
||||||
|
Self {
|
||||||
|
state: Mutex::new(OutboundWriterState {
|
||||||
|
writer,
|
||||||
|
session_key: None,
|
||||||
|
enc_seqnum: 0,
|
||||||
|
frame_buf: BytesMut::with_capacity(128 * 1024),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_session_key(&self, key: secretbox::Key) {
|
||||||
|
self.state.lock().await.session_key = Some(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_plain(&self, data: &[u8]) -> std::io::Result<()> {
|
||||||
|
let mut state = self.state.lock().await;
|
||||||
|
let OutboundWriterState {
|
||||||
|
writer, frame_buf, ..
|
||||||
|
} = &mut *state;
|
||||||
|
write_frame_buffered(writer, data, frame_buf).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_encrypted(&self, data: &[u8]) -> std::io::Result<()> {
|
||||||
|
let mut state = self.state.lock().await;
|
||||||
|
let payload = if let Some(key) = state.session_key.clone() {
|
||||||
|
state.enc_seqnum += 1;
|
||||||
|
let nonce = Connection::get_nonce(state.enc_seqnum);
|
||||||
|
secretbox::seal(data, &nonce, &key)
|
||||||
|
} else {
|
||||||
|
data.to_vec()
|
||||||
|
};
|
||||||
|
let OutboundWriterState {
|
||||||
|
writer, frame_buf, ..
|
||||||
|
} = &mut *state;
|
||||||
|
write_frame_buffered(writer, &payload, frame_buf).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Connection state
|
/// Connection state
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum ConnectionState {
|
pub enum ConnectionState {
|
||||||
@@ -140,8 +230,6 @@ pub struct Connection {
|
|||||||
session_key: Option<secretbox::Key>,
|
session_key: Option<secretbox::Key>,
|
||||||
/// Encryption enabled flag
|
/// Encryption enabled flag
|
||||||
encryption_enabled: bool,
|
encryption_enabled: bool,
|
||||||
/// Encryption sequence number (for nonce generation)
|
|
||||||
enc_seqnum: u64,
|
|
||||||
/// Decryption sequence number (for nonce generation)
|
/// Decryption sequence number (for nonce generation)
|
||||||
dec_seqnum: u64,
|
dec_seqnum: u64,
|
||||||
/// Negotiated video codec (after client capability exchange)
|
/// Negotiated video codec (after client capability exchange)
|
||||||
@@ -234,7 +322,6 @@ impl Connection {
|
|||||||
audio_task: None,
|
audio_task: None,
|
||||||
session_key: None,
|
session_key: None,
|
||||||
encryption_enabled: false,
|
encryption_enabled: false,
|
||||||
enc_seqnum: 0,
|
|
||||||
dec_seqnum: 0,
|
dec_seqnum: 0,
|
||||||
negotiated_codec: None,
|
negotiated_codec: None,
|
||||||
video_frame_tx: None,
|
video_frame_tx: None,
|
||||||
@@ -279,7 +366,7 @@ impl Connection {
|
|||||||
*self.state.write() = ConnectionState::Handshaking;
|
*self.state.write() = ConnectionState::Handshaking;
|
||||||
|
|
||||||
let (mut reader, writer) = stream.into_split();
|
let (mut reader, writer) = stream.into_split();
|
||||||
let writer = Arc::new(Mutex::new(writer));
|
let writer = Arc::new(OutboundWriter::new(writer));
|
||||||
let mut shutdown_rx = self.shutdown_tx.subscribe();
|
let mut shutdown_rx = self.shutdown_tx.subscribe();
|
||||||
|
|
||||||
// Send our SignedId first (this is what RustDesk protocol expects)
|
// Send our SignedId first (this is what RustDesk protocol expects)
|
||||||
@@ -291,23 +378,44 @@ impl Connection {
|
|||||||
debug!("Sending SignedId with device_id={}", self.device_id);
|
debug!("Sending SignedId with device_id={}", self.device_id);
|
||||||
self.send_framed_arc(&writer, &signed_id_bytes).await?;
|
self.send_framed_arc(&writer, &signed_id_bytes).await?;
|
||||||
|
|
||||||
|
// Keep media writes out of the connection read loop. A congested video socket
|
||||||
|
// must not prevent us from reading and dispatching mouse/keyboard messages.
|
||||||
|
let (write_error_tx, mut write_error_rx) = mpsc::unbounded_channel::<String>();
|
||||||
|
|
||||||
// Channel for receiving video frames to send (bounded to provide backpressure)
|
// Channel for receiving video frames to send (bounded to provide backpressure)
|
||||||
let (video_tx, mut video_rx) = mpsc::channel::<Bytes>(4);
|
let (video_tx, mut video_rx) = mpsc::channel::<Bytes>(4);
|
||||||
let mut video_streaming = false;
|
let mut video_streaming = false;
|
||||||
|
let video_writer = writer.clone();
|
||||||
|
let video_error_tx = write_error_tx.clone();
|
||||||
|
let video_sender_task = tokio::spawn(async move {
|
||||||
|
while let Some(frame_data) = video_rx.recv().await {
|
||||||
|
if let Err(error) = video_writer.send_encrypted(&frame_data).await {
|
||||||
|
let _ = video_error_tx.send(format!("video: {}", error));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Channel for receiving audio frames to send (bounded to provide backpressure)
|
// Channel for receiving audio frames to send (bounded to provide backpressure)
|
||||||
let (audio_tx, mut audio_rx) = mpsc::channel::<Bytes>(8);
|
let (audio_tx, mut audio_rx) = mpsc::channel::<Bytes>(8);
|
||||||
let mut audio_streaming = false;
|
let mut audio_streaming = false;
|
||||||
|
let audio_writer = writer.clone();
|
||||||
|
let audio_error_tx = write_error_tx.clone();
|
||||||
|
let audio_sender_task = tokio::spawn(async move {
|
||||||
|
while let Some(frame_data) = audio_rx.recv().await {
|
||||||
|
if let Err(error) = audio_writer.send_encrypted(&frame_data).await {
|
||||||
|
let _ = audio_error_tx.send(format!("audio: {}", error));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
drop(write_error_tx);
|
||||||
|
|
||||||
// Timer for sending TestDelay to measure round-trip latency
|
// Timer for sending TestDelay to measure round-trip latency
|
||||||
// RustDesk clients display this delay information
|
// RustDesk clients display this delay information
|
||||||
let mut test_delay_interval = tokio::time::interval(Duration::from_secs(1));
|
let mut test_delay_interval = tokio::time::interval(Duration::from_secs(1));
|
||||||
test_delay_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
test_delay_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||||
|
|
||||||
// Pre-allocated buffer for framing (reused across sends to reduce allocations)
|
|
||||||
// Typical H264 frame is 10-100KB, pre-allocate 128KB
|
|
||||||
let mut frame_buf = BytesMut::with_capacity(128 * 1024);
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
// Read framed message from client using RustDesk's variable-length encoding
|
// Read framed message from client using RustDesk's variable-length encoding
|
||||||
@@ -334,49 +442,10 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send video frames (encrypted if session key is set)
|
Some(error) = write_error_rx.recv() => {
|
||||||
// Optimized path: inline encryption and use pre-allocated buffer
|
error!("RustDesk outbound write failed: {}", error);
|
||||||
Some(frame_data) = video_rx.recv() => {
|
|
||||||
let send_result = if let Some(ref key) = self.session_key {
|
|
||||||
// Encrypt the frame
|
|
||||||
self.enc_seqnum += 1;
|
|
||||||
let nonce = Self::get_nonce(self.enc_seqnum);
|
|
||||||
let ciphertext = secretbox::seal(&frame_data, &nonce, key);
|
|
||||||
// Send using pre-allocated buffer
|
|
||||||
let mut w = writer.lock().await;
|
|
||||||
write_frame_buffered(&mut *w, &ciphertext, &mut frame_buf).await
|
|
||||||
} else {
|
|
||||||
// No encryption, send plain
|
|
||||||
let mut w = writer.lock().await;
|
|
||||||
write_frame_buffered(&mut *w, &frame_data, &mut frame_buf).await
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = send_result {
|
|
||||||
error!("Error sending video frame: {}", e);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Send audio frames (encrypted if session key is set)
|
|
||||||
Some(frame_data) = audio_rx.recv() => {
|
|
||||||
let send_result = if let Some(ref key) = self.session_key {
|
|
||||||
// Encrypt the frame
|
|
||||||
self.enc_seqnum += 1;
|
|
||||||
let nonce = Self::get_nonce(self.enc_seqnum);
|
|
||||||
let ciphertext = secretbox::seal(&frame_data, &nonce, key);
|
|
||||||
let mut w = writer.lock().await;
|
|
||||||
write_frame_buffered(&mut *w, &ciphertext, &mut frame_buf).await
|
|
||||||
} else {
|
|
||||||
// No encryption, send plain
|
|
||||||
let mut w = writer.lock().await;
|
|
||||||
write_frame_buffered(&mut *w, &frame_data, &mut frame_buf).await
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = send_result {
|
|
||||||
error!("Error sending audio frame: {}", e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send TestDelay periodically to measure latency
|
// Send TestDelay periodically to measure latency
|
||||||
_ = test_delay_interval.tick() => {
|
_ = test_delay_interval.tick() => {
|
||||||
@@ -395,6 +464,9 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
video_sender_task.abort();
|
||||||
|
audio_sender_task.abort();
|
||||||
|
|
||||||
// Stop video streaming task if running
|
// Stop video streaming task if running
|
||||||
if let Some(task) = self.video_task.take() {
|
if let Some(task) = self.video_task.take() {
|
||||||
task.abort();
|
task.abort();
|
||||||
@@ -409,14 +481,13 @@ impl Connection {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send framed message using Arc<Mutex<OwnedWriteHalf>> with RustDesk's variable-length encoding
|
/// Send a plain framed message through the serialized outbound writer.
|
||||||
async fn send_framed_arc(
|
async fn send_framed_arc(
|
||||||
&self,
|
&self,
|
||||||
writer: &Arc<Mutex<OwnedWriteHalf>>,
|
writer: &Arc<OutboundWriter>,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let mut w = writer.lock().await;
|
writer.send_plain(data).await?;
|
||||||
write_frame(&mut *w, data).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,20 +502,11 @@ impl Connection {
|
|||||||
/// RustDesk uses sequence-based nonce, NOT nonce prefix in message
|
/// RustDesk uses sequence-based nonce, NOT nonce prefix in message
|
||||||
async fn send_encrypted_arc(
|
async fn send_encrypted_arc(
|
||||||
&mut self,
|
&mut self,
|
||||||
writer: &Arc<Mutex<OwnedWriteHalf>>,
|
writer: &Arc<OutboundWriter>,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if let Some(ref key) = self.session_key {
|
writer.send_encrypted(data).await?;
|
||||||
// Increment encryption sequence number
|
Ok(())
|
||||||
self.enc_seqnum += 1;
|
|
||||||
let nonce = Self::get_nonce(self.enc_seqnum);
|
|
||||||
// Encrypt the message - RustDesk only sends ciphertext, no nonce prefix
|
|
||||||
let ciphertext = secretbox::seal(data, &nonce, key);
|
|
||||||
self.send_framed_arc(writer, &ciphertext).await
|
|
||||||
} else {
|
|
||||||
// No encryption, send plain
|
|
||||||
self.send_framed_arc(writer, data).await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle incoming message with Arc writer
|
/// Handle incoming message with Arc writer
|
||||||
@@ -452,7 +514,7 @@ impl Connection {
|
|||||||
async fn handle_message_arc(
|
async fn handle_message_arc(
|
||||||
&mut self,
|
&mut self,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
writer: &Arc<Mutex<OwnedWriteHalf>>,
|
writer: &Arc<OutboundWriter>,
|
||||||
video_tx: &mpsc::Sender<Bytes>,
|
video_tx: &mpsc::Sender<Bytes>,
|
||||||
video_streaming: &mut bool,
|
video_streaming: &mut bool,
|
||||||
audio_tx: &mpsc::Sender<Bytes>,
|
audio_tx: &mpsc::Sender<Bytes>,
|
||||||
@@ -576,7 +638,7 @@ impl Connection {
|
|||||||
async fn handle_login_request_arc(
|
async fn handle_login_request_arc(
|
||||||
&mut self,
|
&mut self,
|
||||||
lr: &LoginRequest,
|
lr: &LoginRequest,
|
||||||
writer: &Arc<Mutex<OwnedWriteHalf>>,
|
writer: &Arc<OutboundWriter>,
|
||||||
) -> anyhow::Result<bool> {
|
) -> anyhow::Result<bool> {
|
||||||
info!(
|
info!(
|
||||||
"Login request from {} ({}), password_len={}",
|
"Login request from {} ({}), password_len={}",
|
||||||
@@ -673,7 +735,7 @@ impl Connection {
|
|||||||
async fn handle_misc_arc(
|
async fn handle_misc_arc(
|
||||||
&mut self,
|
&mut self,
|
||||||
misc: &Misc,
|
misc: &Misc,
|
||||||
_writer: &Arc<Mutex<OwnedWriteHalf>>,
|
_writer: &Arc<OutboundWriter>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
match &misc.union {
|
match &misc.union {
|
||||||
Some(misc::Union::SwitchDisplay(sd)) => {
|
Some(misc::Union::SwitchDisplay(sd)) => {
|
||||||
@@ -857,7 +919,7 @@ impl Connection {
|
|||||||
async fn handle_peer_public_key(
|
async fn handle_peer_public_key(
|
||||||
&mut self,
|
&mut self,
|
||||||
pk: &PublicKey,
|
pk: &PublicKey,
|
||||||
writer: &Arc<Mutex<OwnedWriteHalf>>,
|
writer: &Arc<OutboundWriter>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// RustDesk's PublicKey message has two parts:
|
// RustDesk's PublicKey message has two parts:
|
||||||
// - asymmetric_value: The peer's temporary Curve25519 public key (32 bytes)
|
// - asymmetric_value: The peer's temporary Curve25519 public key (32 bytes)
|
||||||
@@ -880,6 +942,7 @@ impl Connection {
|
|||||||
) {
|
) {
|
||||||
Ok(session_key) => {
|
Ok(session_key) => {
|
||||||
info!("Session key negotiated successfully");
|
info!("Session key negotiated successfully");
|
||||||
|
writer.set_session_key(session_key.clone()).await;
|
||||||
self.session_key = Some(session_key);
|
self.session_key = Some(session_key);
|
||||||
self.encryption_enabled = true;
|
self.encryption_enabled = true;
|
||||||
}
|
}
|
||||||
@@ -930,7 +993,7 @@ impl Connection {
|
|||||||
async fn handle_signed_id(
|
async fn handle_signed_id(
|
||||||
&mut self,
|
&mut self,
|
||||||
si: &SignedId,
|
si: &SignedId,
|
||||||
writer: &Arc<Mutex<OwnedWriteHalf>>,
|
writer: &Arc<OutboundWriter>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// The SignedId contains a signed IdPk message
|
// The SignedId contains a signed IdPk message
|
||||||
// Try to parse the IdPk from the signed data
|
// Try to parse the IdPk from the signed data
|
||||||
@@ -1137,7 +1200,7 @@ impl Connection {
|
|||||||
async fn handle_test_delay(
|
async fn handle_test_delay(
|
||||||
&mut self,
|
&mut self,
|
||||||
td: &TestDelay,
|
td: &TestDelay,
|
||||||
writer: &Arc<Mutex<OwnedWriteHalf>>,
|
writer: &Arc<OutboundWriter>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if td.from_client {
|
if td.from_client {
|
||||||
// Client initiated the delay test, respond with the same time
|
// Client initiated the delay test, respond with the same time
|
||||||
@@ -1153,10 +1216,15 @@ impl Connection {
|
|||||||
let data = response
|
let data = response
|
||||||
.write_to_bytes()
|
.write_to_bytes()
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||||
self.send_encrypted_arc(writer, &data).await?;
|
let outbound = writer.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(error) = outbound.send_encrypted(&data).await {
|
||||||
|
warn!("Failed to send TestDelay response: {}", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"TestDelay response sent: time={}, last_delay={}ms",
|
"TestDelay response queued: time={}, last_delay={}ms",
|
||||||
td.time, self.last_delay
|
td.time, self.last_delay
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -1180,7 +1248,7 @@ impl Connection {
|
|||||||
/// The client will echo this back, allowing us to calculate RTT.
|
/// The client will echo this back, allowing us to calculate RTT.
|
||||||
/// The measured delay is then included in future TestDelay messages
|
/// The measured delay is then included in future TestDelay messages
|
||||||
/// for the client to display.
|
/// for the client to display.
|
||||||
async fn send_test_delay(&mut self, writer: &Arc<Mutex<OwnedWriteHalf>>) -> anyhow::Result<()> {
|
async fn send_test_delay(&mut self, writer: &Arc<OutboundWriter>) -> anyhow::Result<()> {
|
||||||
// Get current time in milliseconds since epoch
|
// Get current time in milliseconds since epoch
|
||||||
let time_ms = SystemTime::now()
|
let time_ms = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
@@ -1199,13 +1267,19 @@ impl Connection {
|
|||||||
let data = msg
|
let data = msg
|
||||||
.write_to_bytes()
|
.write_to_bytes()
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||||
self.send_encrypted_arc(writer, &data).await?;
|
let outbound = writer.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(error) = outbound.send_encrypted(&data).await {
|
||||||
|
warn!("Failed to send TestDelay: {}", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Record when we sent this, so we can calculate RTT when client echoes back
|
// Record when we queued this, so we can calculate approximate RTT when the
|
||||||
|
// client echoes it back without blocking the input reader on a media write.
|
||||||
self.last_test_delay_sent = Some(Instant::now());
|
self.last_test_delay_sent = Some(Instant::now());
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"TestDelay sent: time={}, last_delay={}ms",
|
"TestDelay queued: time={}, last_delay={}ms",
|
||||||
time_ms, self.last_delay
|
time_ms, self.last_delay
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1278,34 +1352,57 @@ impl Connection {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle mouse event with throttling
|
/// Handle mouse event with throttling.
|
||||||
///
|
///
|
||||||
/// Pure move events (no button/scroll) are throttled to prevent HID EAGAIN errors.
|
/// Absolute moves may safely be coalesced because the next coordinate catches up.
|
||||||
/// Button down/up and scroll events are always sent immediately.
|
/// Relative moves must be accumulated, otherwise every throttled event permanently
|
||||||
|
/// loses displacement and the remote cursor appears to trail behind the local one.
|
||||||
async fn handle_mouse_event(&mut self, me: &MouseEvent) -> anyhow::Result<()> {
|
async fn handle_mouse_event(&mut self, me: &MouseEvent) -> anyhow::Result<()> {
|
||||||
// Parse RustDesk mask format: (button << 3) | event_type
|
// Parse RustDesk mask format: (button << 3) | event_type
|
||||||
let event_type = me.mask & 0x07;
|
let event_type = me.mask & 0x07;
|
||||||
let is_relative_move = event_type == mouse_type::MOVE_RELATIVE;
|
let is_relative_move = event_type == mouse_type::MOVE_RELATIVE;
|
||||||
|
let is_absolute_move = event_type == mouse_type::MOVE;
|
||||||
|
let is_pure_move = is_absolute_move || is_relative_move;
|
||||||
|
|
||||||
|
let mut mouse_events = Vec::new();
|
||||||
|
|
||||||
if is_relative_move {
|
if is_relative_move {
|
||||||
self.relative_mouse_active = true;
|
self.relative_mouse_active = true;
|
||||||
} else if event_type == mouse_type::MOVE {
|
self.input_throttler.accumulate_relative_move(me.x, me.y);
|
||||||
self.relative_mouse_active = false;
|
if !self.input_throttler.should_send_mouse_move() {
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a pure move event (no button/scroll)
|
|
||||||
let is_pure_move = event_type == mouse_type::MOVE || is_relative_move;
|
|
||||||
|
|
||||||
// For pure move events, apply throttling
|
|
||||||
if is_pure_move && !self.input_throttler.should_send_mouse_move() {
|
|
||||||
// Skip this move event to prevent HID EAGAIN
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
if let Some((x, y)) = self.input_throttler.take_relative_move() {
|
||||||
|
mouse_events.extend(convert_relative_mouse_delta(x, y));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if is_absolute_move {
|
||||||
|
self.relative_mouse_active = false;
|
||||||
|
self.input_throttler.clear_relative_move();
|
||||||
|
if !self.input_throttler.should_send_mouse_move() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
} else if self.relative_mouse_active {
|
||||||
|
// Preserve ordering: flush movement accumulated immediately before a
|
||||||
|
// button or wheel event so the action occurs at the intended position.
|
||||||
|
if let Some((x, y)) = self.input_throttler.take_relative_move() {
|
||||||
|
mouse_events.extend(convert_relative_mouse_delta(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mouse_events.extend(convert_mouse_event(
|
||||||
|
me,
|
||||||
|
self.screen_width,
|
||||||
|
self.screen_height,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
debug!("Mouse event: x={}, y={}, mask={}", me.x, me.y, me.mask);
|
debug!(
|
||||||
|
"Mouse event: x={}, y={}, mask={}, hid_reports={}",
|
||||||
// Convert RustDesk mouse event to One-KVM mouse events
|
me.x,
|
||||||
let mouse_events = convert_mouse_event(me, self.screen_width, self.screen_height);
|
me.y,
|
||||||
|
me.mask,
|
||||||
|
mouse_events.len()
|
||||||
|
);
|
||||||
|
|
||||||
// Send to HID controller if available
|
// Send to HID controller if available
|
||||||
if let Some(ref hid) = self.hid {
|
if let Some(ref hid) = self.hid {
|
||||||
|
|||||||
@@ -50,13 +50,7 @@ pub fn convert_mouse_event(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
mouse_type::MOVE_RELATIVE => {
|
mouse_type::MOVE_RELATIVE => {
|
||||||
events.push(OneKvmMouseEvent {
|
events.extend(convert_relative_mouse_delta(event.x, event.y));
|
||||||
event_type: MouseEventType::Move,
|
|
||||||
x: event.x,
|
|
||||||
y: event.y,
|
|
||||||
button: None,
|
|
||||||
scroll: 0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
mouse_type::DOWN => {
|
mouse_type::DOWN => {
|
||||||
if let Some(button) = button_id_to_button(button_id) {
|
if let Some(button) = button_id_to_button(button_id) {
|
||||||
@@ -96,6 +90,28 @@ pub fn convert_mouse_event(
|
|||||||
events
|
events
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Split accumulated relative movement into USB HID-sized reports. Both the OTG
|
||||||
|
/// and CH9329 relative endpoints are limited to signed 8-bit deltas.
|
||||||
|
pub fn convert_relative_mouse_delta(mut x: i32, mut y: i32) -> Vec<OneKvmMouseEvent> {
|
||||||
|
let mut events = Vec::new();
|
||||||
|
|
||||||
|
while x != 0 || y != 0 {
|
||||||
|
let dx = x.clamp(-127, 127);
|
||||||
|
let dy = y.clamp(-127, 127);
|
||||||
|
events.push(OneKvmMouseEvent {
|
||||||
|
event_type: MouseEventType::Move,
|
||||||
|
x: dx,
|
||||||
|
y: dy,
|
||||||
|
button: None,
|
||||||
|
scroll: 0,
|
||||||
|
});
|
||||||
|
x -= dx;
|
||||||
|
y -= dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
events
|
||||||
|
}
|
||||||
|
|
||||||
fn button_id_to_button(button_id: i32) -> Option<MouseButton> {
|
fn button_id_to_button(button_id: i32) -> Option<MouseButton> {
|
||||||
match button_id {
|
match button_id {
|
||||||
mouse_button::LEFT => Some(MouseButton::Left),
|
mouse_button::LEFT => Some(MouseButton::Left),
|
||||||
@@ -611,6 +627,17 @@ mod tests {
|
|||||||
assert_eq!(events[0].y, 8);
|
assert_eq!(events[0].y, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_large_relative_move_is_split_without_losing_delta() {
|
||||||
|
let events = convert_relative_mouse_delta(300, -260);
|
||||||
|
assert_eq!(events.len(), 3);
|
||||||
|
assert!(events
|
||||||
|
.iter()
|
||||||
|
.all(|event| event.x.abs() <= 127 && event.y.abs() <= 127));
|
||||||
|
assert_eq!(events.iter().map(|event| event.x).sum::<i32>(), 300);
|
||||||
|
assert_eq!(events.iter().map(|event| event.y).sum::<i32>(), -260);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_convert_key_event() {
|
fn test_convert_key_event() {
|
||||||
use protobuf::EnumOrUnknown;
|
use protobuf::EnumOrUnknown;
|
||||||
|
|||||||
Reference in New Issue
Block a user