diff --git a/crates/client-core/src/connection.rs b/crates/client-core/src/connection.rs index 1698ad5..a013c69 100644 --- a/crates/client-core/src/connection.rs +++ b/crates/client-core/src/connection.rs @@ -225,6 +225,31 @@ pub struct TurnConfig { pub struct NatTraversal; impl NatTraversal { + /// 获取所有本地 IP 地址 + pub fn get_local_ips() -> Vec { + let mut ips = Vec::new(); + + // 尝试使用 socket 连接来获取本地 IP + if let Ok(socket) = std::net::UdpSocket::bind("0.0.0.0:0") { + // 连接到公网地址来获取本机实际使用的 IP + if let Ok(()) = socket.connect("8.8.8.8:80") { + if let Ok(addr) = socket.local_addr() { + let ip = addr.ip().to_string(); + if !ip.starts_with("127.") && ip != "0.0.0.0" { + ips.push(ip); + } + } + } + } + + // 如果没有获取到,添加 localhost + if ips.is_empty() { + ips.push("127.0.0.1".to_string()); + } + + ips + } + /// 从服务器获取 ICE 服务器配置 pub async fn fetch_ice_servers(server_url: &str) -> Result { // 将 ws:// 或 wss:// 转换为 http:// 或 https:// diff --git a/crates/client-core/src/signal.rs b/crates/client-core/src/signal.rs index ec53bfe..1754115 100644 --- a/crates/client-core/src/signal.rs +++ b/crates/client-core/src/signal.rs @@ -19,6 +19,14 @@ pub struct DisplayInfoMsg { pub name: String, } +/// TURN 服务器信息(用于 P2P 中继) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TurnServerInfo { + pub url: String, + pub username: String, + pub credential: String, +} + /// 信令消息 #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] @@ -162,6 +170,38 @@ pub enum SignalMessage { fps: u32, quality: u32, }, + /// P2P 地址交换请求 + #[serde(rename = "p2p_exchange")] + P2PExchange { + session_id: String, + from_device: String, + to_device: String, + /// 本地地址列表 (局域网) + local_addrs: Vec, + /// 公网地址 (通过 STUN 获取) + public_addr: Option, + /// UDP 监听端口 + udp_port: u16, + /// TURN 服务器信息(用于中继) + turn_server: Option, + }, + /// P2P 连接就绪 + #[serde(rename = "p2p_ready")] + P2PReady { + session_id: String, + from_device: String, + to_device: String, + /// 成功连接的地址 + connected_addr: String, + }, + /// P2P 连接失败,回退到服务器中转 + #[serde(rename = "p2p_fallback")] + P2PFallback { + session_id: String, + from_device: String, + to_device: String, + reason: String, + }, } /// 信令客户端 @@ -182,6 +222,11 @@ impl SignalClient { } } + /// 获取服务器 URL + pub fn server_url(&self) -> Option { + Some(self.server_url.clone()) + } + /// 连接到信令服务器 pub async fn connect( &mut self, @@ -222,14 +267,42 @@ impl SignalClient { tokio::spawn(async move { while let Some(Ok(msg)) = read.next().await { if let Message::Text(text) = msg { - println!("★★★ [客户端] 收到WebSocket消息: {}", &text[..text.len().min(200)]); + // 检测消息类型 + let is_heartbeat = text.contains("heartbeat"); + let is_mouse = text.contains("mouse_event"); + let is_keyboard = text.contains("keyboard_event"); + let is_screen = text.contains("screen_frame"); + + // 打印控制消息(鼠标、键盘) + if is_mouse || is_keyboard { + println!("★★★ [被控端] 收到控制消息: {}", &text[..text.len().min(200)]); + } else if !is_heartbeat && !is_screen { + println!("★★★ [客户端] 收到WebSocket消息: {}", &text[..text.len().min(300)]); + } + match serde_json::from_str::(&text) { Ok(signal_msg) => { - println!("★★★ [客户端] 解析成功: {:?}", std::mem::discriminant(&signal_msg)); + // 打印非心跳、非屏幕帧消息 + match &signal_msg { + SignalMessage::HeartbeatAck => {}, + SignalMessage::Heartbeat { .. } => {}, + SignalMessage::ScreenFrame { .. } => {}, + SignalMessage::MouseEvent { x, y, event_type, .. } => { + if event_type != "move" { + println!("★★★ [被控端] 解析鼠标事件成功: type={}, x={:.0}, y={:.0}", event_type, x, y); + } + } + SignalMessage::KeyboardEvent { key, event_type, .. } => { + println!("★★★ [被控端] 解析键盘事件成功: key={}, type={}", key, event_type); + } + other => { + println!("★★★ [客户端] 解析成功: {:?}", std::mem::discriminant(other)); + } + } on_message(signal_msg); } Err(e) => { - println!("★★★ [客户端] 解析失败: {}, 原始消息: {}", e, &text[..text.len().min(100)]); + println!("★★★ [客户端] 解析失败: {}, 原始消息: {}", e, &text[..text.len().min(200)]); } } } diff --git a/crates/client-tauri/Cargo.toml b/crates/client-tauri/Cargo.toml index 37fd166..870786d 100644 --- a/crates/client-tauri/Cargo.toml +++ b/crates/client-tauri/Cargo.toml @@ -11,7 +11,7 @@ easyremote-common = { path = "../common" } easyremote-client-core = { path = "../client-core" } # Tauri -tauri = { version = "1.5", features = ["shell-open", "dialog-all", "clipboard-all", "window-all", "global-shortcut-all", "process-all", "system-tray"] } +tauri = { version = "1.5", features = ["shell-open", "dialog-all", "clipboard-all", "window-all", "global-shortcut-all", "process-all", "system-tray", "window-data-url"] } # Windows single instance [target.'cfg(windows)'.dependencies] @@ -46,6 +46,8 @@ thiserror = { workspace = true } dirs = "5.0" image = { workspace = true } once_cell = "1.19" +urlencoding = "2.1" +url = "2.5" [features] default = ["custom-protocol"] diff --git a/crates/client-tauri/src/commands.rs b/crates/client-tauri/src/commands.rs index 5986821..bad5a94 100644 --- a/crates/client-tauri/src/commands.rs +++ b/crates/client-tauri/src/commands.rs @@ -1,11 +1,14 @@ //! Tauri 命令 -use crate::state::{AppState, ConnectionState, CurrentUser, DeviceInfo, HistoryItem, ConnectRequest}; +use crate::state::{AppState, ConnectionState, CurrentUser, DeviceInfo, HistoryItem, ConnectRequest, P2PConnection}; use easyremote_client_core::{ClientConfig, SignalClient, SignalMessage, ScreenCapturer}; +use easyremote_client_core::connection::NatTraversal; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::net::SocketAddr; use tauri::State; use tokio::sync::RwLock; +use tokio::net::UdpSocket; type AppStateHandle = Arc>; @@ -29,9 +32,9 @@ struct StreamSettings { impl Default for StreamSettings { fn default() -> Self { Self { - resolution: 0.5, - fps: 15, - quality: 70, + resolution: 0.5, // 50% 分辨率,平衡质量和性能 + fps: 30, // 30 FPS 提供流畅体验 + quality: 50, // 50% JPEG 质量,大幅减少数据量和编码时间 } } } @@ -81,6 +84,189 @@ async fn get_signal_client() -> &'static Arc>> { }).await } +/// P2P UDP 监听器 +static P2P_LISTENER: tokio::sync::OnceCell>>> = tokio::sync::OnceCell::const_new(); + +/// P2P 监听器 +struct P2PListener { + socket: Arc, + local_port: u16, + local_ips: Vec, +} + +async fn get_p2p_listener() -> &'static Arc>> { + P2P_LISTENER.get_or_init(|| async { + Arc::new(RwLock::new(None)) + }).await +} + +/// 初始化 P2P 监听器 +async fn init_p2p_listener() -> Result<(u16, Vec), String> { + let listener = get_p2p_listener().await; + let mut lock = listener.write().await; + + // 如果已经初始化,返回现有信息 + if let Some(ref l) = *lock { + return Ok((l.local_port, l.local_ips.clone())); + } + + // 创建 UDP socket + let socket = UdpSocket::bind("0.0.0.0:0").await.map_err(|e| e.to_string())?; + let local_addr = socket.local_addr().map_err(|e| e.to_string())?; + let local_port = local_addr.port(); + + // 获取本地 IP 地址列表 + let local_ips = NatTraversal::get_local_ips(); + + tracing::info!("P2P 监听器已初始化: port={}, ips={:?}", local_port, local_ips); + + let socket_arc = Arc::new(socket); + let socket_for_listener = socket_arc.clone(); + + *lock = Some(P2PListener { + socket: socket_arc, + local_port, + local_ips: local_ips.clone(), + }); + + // 启动 P2P 包监听任务 + tokio::spawn(async move { + let mut buf = [0u8; 65536]; + loop { + match socket_for_listener.recv_from(&mut buf).await { + Ok((len, from)) => { + // 检查是否是打洞包 + if len >= 9 && &buf[..9] == b"P2P_PUNCH" { + println!("★★★ 收到 P2P 打洞包来自: {}", from); + // 响应打洞包 + let _ = socket_for_listener.send_to(b"P2P_ACK", from).await; + } else if len > 9 { + // 可能是屏幕帧数据或输入事件 + // TODO: 处理 P2P 数据传输 + println!("★★★ 收到 P2P 数据包: {} 字节 来自: {}", len, from); + } + } + Err(e) => { + tracing::warn!("P2P 监听错误: {}", e); + break; + } + } + } + }); + + Ok((local_port, local_ips)) +} + +/// 获取公网地址(通过 STUN) +async fn get_public_addr(server_url: &str) -> Option { + let (public_addr, _) = get_ice_info(server_url).await; + public_addr +} + +/// 获取 ICE 信息(公网地址 + TURN 服务器) +async fn get_ice_info(server_url: &str) -> (Option, Option) { + use easyremote_client_core::signal::TurnServerInfo; + + let mut public_addr = None; + let mut turn_server = None; + + // 尝试从服务器获取 ICE 服务器配置 + match NatTraversal::fetch_ice_servers(server_url).await { + Ok(config) => { + // 获取公网地址(通过 STUN) + for stun in &config.stun_servers { + match NatTraversal::get_public_addr_from_url(stun).await { + Ok(addr) => { + tracing::info!("获取到公网地址: {} (来自 {})", addr, stun); + println!("★★★ 获取到公网地址: {} (来自 {})", addr, stun); + public_addr = Some(addr.to_string()); + break; + } + Err(e) => { + tracing::warn!("从 STUN {} 获取公网地址失败: {}", stun, e); + } + } + } + + // 获取 TURN 服务器信息 + if let Some(turn) = config.turn_server { + tracing::info!("获取到 TURN 服务器: {}", turn.url); + println!("★★★ 获取到 TURN 服务器: {}", turn.url); + turn_server = Some(TurnServerInfo { + url: turn.url, + username: turn.username, + credential: turn.credential, + }); + } + } + Err(e) => { + tracing::warn!("获取 ICE 服务器配置失败: {}", e); + } + } + + (public_addr, turn_server) +} + +/// 尝试建立 P2P 连接(增强版 - 双向打洞 + 更多重试) +async fn try_p2p_connect(socket: &UdpSocket, target: SocketAddr) -> bool { + // 发送打洞包 + let punch_packet = b"P2P_PUNCH"; + let mut success = false; + + // 增加重试次数和双向打洞 + for attempt in 0..5 { + // 快速发送多个打洞包(模拟突发,提高穿透率) + for _ in 0..3 { + let _ = socket.send_to(punch_packet, target).await; + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + + // 等待响应(超时 800ms) + let mut buf = [0u8; 64]; + let deadline = std::time::Instant::now() + std::time::Duration::from_millis(800); + + while std::time::Instant::now() < deadline { + match tokio::time::timeout( + std::time::Duration::from_millis(200), + socket.recv_from(&mut buf) + ).await { + Ok(Ok((len, from))) => { + // 检查是否是来自目标的响应或打洞包 + if len >= 7 && &buf[..7] == b"P2P_ACK" { + tracing::info!("P2P 打洞成功: {} (ACK 来自 {})", target, from); + println!("★★★ P2P 打洞成功: {} (ACK 来自 {})", target, from); + return true; + } else if len >= 9 && &buf[..9] == b"P2P_PUNCH" { + // 收到对方的打洞包,立即响应 + let _ = socket.send_to(b"P2P_ACK", from).await; + tracing::info!("P2P 收到打洞包,已响应: {}", from); + println!("★★★ P2P 收到打洞包来自: {},已响应 ACK", from); + success = true; + } + } + Ok(Err(_)) => break, + Err(_) => {} + } + } + + if success { + // 再发送几次确认包 + for _ in 0..2 { + let _ = socket.send_to(b"P2P_ACK", target).await; + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + return true; + } + + // 指数退避等待 + let wait_time = 100 * (1 << attempt.min(3)); + tokio::time::sleep(std::time::Duration::from_millis(wait_time)).await; + } + + println!("★★★ P2P 打洞失败: {} (尝试 5 次)", target); + false +} + /// 检查是否需要强制下线 pub fn check_force_offline() -> bool { FORCE_OFFLINE_FLAG.swap(false, Ordering::SeqCst) @@ -99,7 +285,7 @@ fn set_force_offline() { tracing::info!("已清除登录信息"); } -/// 启动屏幕流 +/// 启动屏幕流(优化版 - 低延迟) async fn start_screen_streaming(session_id: String, controller_device: String, my_device_id: String) { tracing::info!("★★★ 启动屏幕流: session={}, controller={}, my_device={}", session_id, controller_device, my_device_id); println!("★★★ 启动屏幕流: session={}, controller={}, my_device={}", session_id, controller_device, my_device_id); @@ -143,8 +329,11 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m std::thread::spawn(move || { println!("★★★ 开始创建屏幕捕获器..."); - // 创建一个 tokio runtime 用于发送帧 - let rt = match tokio::runtime::Runtime::new() { + // 创建一个多线程 tokio runtime 用于异步发送帧 + let rt = match tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build() { Ok(r) => r, Err(e) => { println!("★★★ 创建 tokio runtime 失败: {}", e); @@ -174,21 +363,44 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m let mut frame_count = 0u64; let start_time = std::time::Instant::now(); + let mut last_frame_time = std::time::Instant::now(); + + // 创建发送通道(异步发送,不阻塞捕获) + let (frame_tx, mut frame_rx) = tokio::sync::mpsc::channel::<(u32, u32, Vec)>(2); + + // 启动异步发送任务 + let session_id_send = session_id_capture.clone(); + let controller_device_send = controller_device_capture.clone(); + let signal_client_send = signal_client_clone.clone(); + rt.spawn(async move { + while let Some((w, h, jpeg_data)) = frame_rx.recv().await { + let client = signal_client_send.read().await; + if let Some(c) = client.as_ref() { + let _ = c.send_frame( + session_id_send.clone(), + controller_device_send.clone(), + w, + h, + &jpeg_data, + ).await; + } + } + }); loop { + let frame_start = std::time::Instant::now(); + // 获取当前设置 - let (resolution, fps, _quality) = { + let (resolution, fps, quality) = { let s = stream_settings.read().unwrap(); (s.resolution, s.fps, s.quality) }; - // 计算帧间隔 - let frame_interval = std::time::Duration::from_millis(1000 / fps as u64); + // 计算目标帧间隔 + let target_interval = std::time::Duration::from_micros(1_000_000 / fps as u64); // 检查是否需要停止 - let should_stop = stop_flag_clone.load(Ordering::SeqCst); - - if should_stop { + if stop_flag_clone.load(Ordering::SeqCst) { tracing::info!("屏幕流停止"); break; } @@ -196,55 +408,54 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m // 捕获帧 match capturer.capture_rgba() { Ok(Some(rgba_data)) => { - // 使用 image crate 压缩为 JPEG - let img = image::RgbaImage::from_raw(width, height, rgba_data); - if let Some(img) = img { - let mut jpeg_data = Vec::new(); - let mut cursor = std::io::Cursor::new(&mut jpeg_data); + // 计算目标分辨率 + let scaled_width = ((width as f64) * resolution) as u32; + let scaled_height = ((height as f64) * resolution) as u32; + + // 使用更高效的编码流程 + let jpeg_data = if resolution >= 0.99 { + // 不需要缩放,直接编码 + encode_jpeg_fast(&rgba_data, width, height, quality) + } else { + // 需要缩放 + let img = image::RgbaImage::from_raw(width, height, rgba_data); + if let Some(img) = img { + // 使用最快的缩放算法 + let scaled = image::imageops::resize( + &img, + scaled_width, + scaled_height, + image::imageops::FilterType::Nearest + ); + encode_jpeg_fast(scaled.as_raw(), scaled_width, scaled_height, quality) + } else { + None + } + }; + + if let Some(jpeg_data) = jpeg_data { + let jpeg_len = jpeg_data.len(); - // 根据设置调整分辨率 - let scaled_width = ((width as f64) * resolution) as u32; - let scaled_height = ((height as f64) * resolution) as u32; - let scaled = image::imageops::resize(&img, scaled_width, scaled_height, image::imageops::FilterType::Nearest); + // 异步发送(不阻塞) + let _ = frame_tx.try_send((scaled_width, scaled_height, jpeg_data)); - if scaled.write_to(&mut cursor, image::ImageFormat::Jpeg).is_ok() { - let jpeg_len = jpeg_data.len(); - - // 发送帧 - let session_id_send = session_id_capture.clone(); - let controller_device_send = controller_device_capture.clone(); - let signal_client_send = signal_client_clone.clone(); - - rt.block_on(async { - let client = signal_client_send.read().await; - if let Some(c) = client.as_ref() { - let _ = c.send_frame( - session_id_send, - controller_device_send, - scaled_width, - scaled_height, - &jpeg_data, - ).await; - } - }); - - frame_count += 1; - if frame_count % 30 == 0 { - let actual_fps = frame_count as f64 / start_time.elapsed().as_secs_f64(); - println!("★★★ 屏幕流: {:.1} fps, 分辨率: {}x{}, 帧大小: {} KB", - actual_fps, scaled_width, scaled_height, jpeg_len / 1024); - tracing::debug!("屏幕流: {:.1} fps, 分辨率: {}x{}, 帧大小: {} KB", - actual_fps, scaled_width, scaled_height, jpeg_len / 1024); - } - // 首帧日志 - if frame_count == 1 { - println!("★★★ 发送第一帧: {}x{}, {} 字节", scaled_width, scaled_height, jpeg_len); - } + frame_count += 1; + if frame_count % 30 == 0 { + let elapsed = start_time.elapsed().as_secs_f64(); + let actual_fps = frame_count as f64 / elapsed; + let frame_time = frame_start.elapsed().as_millis(); + println!("★★★ 屏幕流: {:.1} fps, {}x{}, {} KB, 帧处理: {}ms", + actual_fps, scaled_width, scaled_height, jpeg_len / 1024, frame_time); + } + if frame_count == 1 { + println!("★★★ 发送第一帧: {}x{}, {} 字节", scaled_width, scaled_height, jpeg_len); } } } Ok(None) => { - // 没有新帧 + // 没有新帧,短暂等待 + std::thread::sleep(std::time::Duration::from_micros(500)); + continue; } Err(e) => { tracing::error!("捕获屏幕失败: {}", e); @@ -252,15 +463,42 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m } } - // 根据设置控制帧率 - std::thread::sleep(frame_interval); + // 精确帧率控制(考虑处理时间) + let elapsed = last_frame_time.elapsed(); + if elapsed < target_interval { + let sleep_time = target_interval - elapsed; + if sleep_time > std::time::Duration::from_micros(500) { + std::thread::sleep(sleep_time - std::time::Duration::from_micros(200)); + } + } + last_frame_time = std::time::Instant::now(); } - // 清理完成,stop_flag 已设置为 true println!("★★★ 屏幕流线程结束"); }); } +/// 快速 JPEG 编码(使用较低质量以减少延迟) +fn encode_jpeg_fast(rgba_data: &[u8], width: u32, height: u32, quality: u32) -> Option> { + use image::codecs::jpeg::JpegEncoder; + + // RGBA -> RGB(JPEG 不支持透明通道) + let rgb_data: Vec = rgba_data + .chunks(4) + .flat_map(|chunk| [chunk[0], chunk[1], chunk[2]]) + .collect(); + + let mut jpeg_data = Vec::with_capacity(width as usize * height as usize / 4); + { + // 使用指定质量编码 + let mut encoder = JpegEncoder::new_with_quality(&mut jpeg_data, quality as u8); + if encoder.encode(&rgb_data, width, height, image::ColorType::Rgb8.into()).is_err() { + return None; + } + } + Some(jpeg_data) +} + /// 处理鼠标输入 fn handle_mouse_input(x: f64, y: f64, event_type: &str, button: Option, delta: Option) { use easyremote_client_core::InputController; @@ -274,44 +512,79 @@ fn handle_mouse_input(x: f64, y: f64, event_type: &str, button: Option, delt let actual_x = (x * scale) as i32; let actual_y = (y * scale) as i32; - if let Ok(mut controller) = InputController::new() { - match event_type { - "move" => { - let _ = controller.move_mouse(actual_x, actual_y); - } - "click" => { - let btn = match button { - Some(0) => easyremote_client_core::MouseButton::Left, - Some(2) => easyremote_client_core::MouseButton::Right, - Some(1) => easyremote_client_core::MouseButton::Middle, - _ => easyremote_client_core::MouseButton::Left, - }; - let _ = controller.click(btn); - } - "down" => { - let btn = match button { - Some(0) => easyremote_client_core::MouseButton::Left, - Some(2) => easyremote_client_core::MouseButton::Right, - Some(1) => easyremote_client_core::MouseButton::Middle, - _ => easyremote_client_core::MouseButton::Left, - }; - let _ = controller.mouse_down(btn); - } - "up" => { - let btn = match button { - Some(0) => easyremote_client_core::MouseButton::Left, - Some(2) => easyremote_client_core::MouseButton::Right, - Some(1) => easyremote_client_core::MouseButton::Middle, - _ => easyremote_client_core::MouseButton::Left, - }; - let _ = controller.mouse_up(btn); - } - "scroll" => { - if let Some(d) = delta { - let _ = controller.scroll(0, -(d as i32) / 3); + // 调试日志(仅对非移动事件打印) + if event_type != "move" { + println!("★★★★★ [被控端] 执行鼠标操作: type={}, raw=({:.0}, {:.0}), scaled=({}, {})", + event_type, x, y, actual_x, actual_y); + } + + match InputController::new() { + Ok(mut controller) => { + // 所有事件都需要先移动鼠标到目标位置 + match controller.move_mouse(actual_x, actual_y) { + Ok(_) => { + if event_type != "move" { + println!("★★★★★ [被控端] 鼠标已移动到 ({}, {})", actual_x, actual_y); + } + } + Err(e) => { + println!("★★★★★ [被控端] 移动鼠标到 ({}, {}) 失败: {}", actual_x, actual_y, e); } } - _ => {} + + match event_type { + "move" => { + // 已经在上面移动了 + } + "click" => { + let btn = match button { + Some(0) => easyremote_client_core::MouseButton::Left, + Some(2) => easyremote_client_core::MouseButton::Right, + Some(1) => easyremote_client_core::MouseButton::Middle, + _ => easyremote_client_core::MouseButton::Left, + }; + match controller.click(btn) { + Ok(_) => println!("★★★★★ [被控端] 鼠标点击成功"), + Err(e) => println!("★★★★★ [被控端] 点击鼠标失败: {}", e), + } + } + "down" => { + let btn = match button { + Some(0) => easyremote_client_core::MouseButton::Left, + Some(2) => easyremote_client_core::MouseButton::Right, + Some(1) => easyremote_client_core::MouseButton::Middle, + _ => easyremote_client_core::MouseButton::Left, + }; + match controller.mouse_down(btn) { + Ok(_) => println!("★★★★★ [被控端] 鼠标按下成功"), + Err(e) => println!("★★★★★ [被控端] 鼠标按下失败: {}", e), + } + } + "up" => { + let btn = match button { + Some(0) => easyremote_client_core::MouseButton::Left, + Some(2) => easyremote_client_core::MouseButton::Right, + Some(1) => easyremote_client_core::MouseButton::Middle, + _ => easyremote_client_core::MouseButton::Left, + }; + match controller.mouse_up(btn) { + Ok(_) => println!("★★★★★ [被控端] 鼠标释放成功"), + Err(e) => println!("★★★★★ [被控端] 鼠标释放失败: {}", e), + } + } + "scroll" => { + if let Some(d) = delta { + match controller.scroll(0, -(d as i32) / 3) { + Ok(_) => println!("★★★★★ [被控端] 滚动成功"), + Err(e) => println!("★★★★★ [被控端] 滚动失败: {}", e), + } + } + } + _ => {} + } + } + Err(e) => { + println!("★★★★★ [被控端] 创建 InputController 失败: {}", e); } } } @@ -320,15 +593,24 @@ fn handle_mouse_input(x: f64, y: f64, event_type: &str, button: Option, delt fn handle_keyboard_input(key: &str, event_type: &str) { use easyremote_client_core::InputController; - if let Ok(mut controller) = InputController::new() { - match event_type { - "down" => { - let _ = controller.key_down(key); + match InputController::new() { + Ok(mut controller) => { + match event_type { + "down" => { + if let Err(e) = controller.key_down(key) { + println!("★★★ 键盘按下失败: {} - {}", key, e); + } + } + "up" => { + if let Err(e) = controller.key_up(key) { + println!("★★★ 键盘释放失败: {} - {}", key, e); + } + } + _ => {} } - "up" => { - let _ = controller.key_up(key); - } - _ => {} + } + Err(e) => { + println!("★★★ 创建 InputController 失败: {}", e); } } } @@ -414,23 +696,67 @@ async fn connect_to_signal_server(device_id: String, server_url: String) -> Resu SignalMessage::ConnectRequest { session_id, from_device, to_device, .. } => { tracing::info!("★★★ 收到连接请求: session_id={}, from={}, to={}", session_id, from_device, to_device); println!("★★★ 收到连接请求: session_id={}, from={}, to={}", session_id, from_device, to_device); - // 启动屏幕流 + + // 启动屏幕流(先通过服务器中转,同时尝试建立 P2P) let session_id_clone = session_id.clone(); let from_device_clone = from_device.clone(); let device_id_stream = device_id_inner.clone(); + + // 启动 P2P 地址交换 + let session_id_p2p = session_id.clone(); + let from_device_p2p = from_device.clone(); + let device_id_p2p = device_id_inner.clone(); + std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async move { - start_screen_streaming(session_id_clone, from_device_clone, device_id_stream).await; + // 先启动屏幕流(通过服务器中转) + start_screen_streaming(session_id_clone, from_device_clone.clone(), device_id_stream.clone()).await; + + // 初始化 P2P 监听器并发送地址交换消息 + match init_p2p_listener().await { + Ok((local_port, local_ips)) => { + // 获取公网地址和 TURN 服务器信息 + let signal_client_holder = get_signal_client().await; + let client = signal_client_holder.read().await; + if let Some(c) = client.as_ref() { + // 获取服务器 URL + if let Some(server_url) = c.server_url() { + // 获取 ICE 服务器配置(包含 STUN 和 TURN) + let (public_addr, turn_server) = get_ice_info(&server_url).await; + + // 发送 P2P 地址交换消息 + let exchange_msg = SignalMessage::P2PExchange { + session_id: session_id_p2p, + from_device: device_id_p2p, + to_device: from_device_p2p, + local_addrs: local_ips, + public_addr, + udp_port: local_port, + turn_server, + }; + let _ = c.send(exchange_msg).await; + tracing::info!("★★★ 已发送 P2P 地址交换消息"); + } + } + } + Err(e) => { + tracing::warn!("初始化 P2P 监听器失败: {}", e); + } + } }); }); } SignalMessage::MouseEvent { x, y, event_type, button, delta, .. } => { // 处理鼠标事件 + if event_type != "move" { + println!("★★★★★ [被控端] 收到鼠标事件: x={}, y={}, type={}, button={:?}", x, y, event_type, button); + } handle_mouse_input(x, y, &event_type, button, delta); } SignalMessage::KeyboardEvent { key, event_type, .. } => { // 处理键盘事件 + println!("★★★★★ [被控端] 收到键盘事件: key={}, type={}", key, event_type); handle_keyboard_input(&key, &event_type); } SignalMessage::StreamSettings { resolution, fps, quality, .. } => { @@ -530,6 +856,111 @@ async fn connect_to_signal_server(device_id: String, server_url: String) -> Resu } }); } + SignalMessage::P2PExchange { session_id, from_device, local_addrs, public_addr, udp_port, turn_server, .. } => { + // 控制端收到被控端的地址信息,尝试建立 P2P 连接 + println!("★★★ 收到 P2P 地址交换: session_id={}, from={}", session_id, from_device); + println!(" 本地地址: {:?}", local_addrs); + println!(" 公网地址: {:?}", public_addr); + println!(" UDP 端口: {}", udp_port); + if let Some(ref turn) = turn_server { + println!(" TURN 服务器: {}", turn.url); + } + + let device_id_p2p = device_id_inner.clone(); + let session_id_p2p = session_id.clone(); + let from_device_p2p = from_device.clone(); + let turn_info = turn_server.clone(); + + tokio::spawn(async move { + // 尝试连接到对方的各个地址 + let mut connected = false; + let mut connected_addr = String::new(); + let mut connection_type = "p2p"; // "p2p" 或 "relay" + + // 创建本地 UDP socket + if let Ok(socket) = UdpSocket::bind("0.0.0.0:0").await { + // 首先尝试局域网地址(延迟最低) + for ip in &local_addrs { + let addr = format!("{}:{}", ip, udp_port); + if let Ok(target) = addr.parse::() { + println!("★★★ 尝试连接局域网地址: {}", addr); + if try_p2p_connect(&socket, target).await { + connected = true; + connected_addr = addr; + connection_type = "p2p-lan"; + println!("★★★ P2P 局域网连接成功: {}", connected_addr); + break; + } + } + } + + // 如果局域网连接失败,尝试公网地址(UDP 打洞) + if !connected { + if let Some(ref public) = public_addr { + // 公网地址可能是 "ip:port" 格式 + let addr = if public.contains(':') { + // 替换端口 + let parts: Vec<&str> = public.split(':').collect(); + format!("{}:{}", parts[0], udp_port) + } else { + format!("{}:{}", public, udp_port) + }; + + if let Ok(target) = addr.parse::() { + println!("★★★ 尝试连接公网地址: {}", addr); + if try_p2p_connect(&socket, target).await { + connected = true; + connected_addr = addr; + connection_type = "p2p-wan"; + println!("★★★ P2P 公网连接成功: {}", connected_addr); + } + } + } + } + } + + // 如果 P2P 直连失败,记录 TURN 服务器信息(备用) + if !connected { + if let Some(ref turn) = turn_info { + println!("★★★ P2P 直连失败,可使用 TURN 中继: {}", turn.url); + tracing::info!("P2P 直连失败,TURN 服务器可用: {}", turn.url); + // 注意:当前使用服务器 WebSocket 中转,TURN 中继可用于未来优化 + } + } + + // 发送连接结果 + let signal_client_holder = get_signal_client().await; + let client = signal_client_holder.read().await; + if let Some(c) = client.as_ref() { + if connected { + let msg = SignalMessage::P2PReady { + session_id: session_id_p2p, + from_device: device_id_p2p, + to_device: from_device_p2p, + connected_addr: format!("{} ({})", connected_addr, connection_type), + }; + let _ = c.send(msg).await; + } else { + let msg = SignalMessage::P2PFallback { + session_id: session_id_p2p, + from_device: device_id_p2p, + to_device: from_device_p2p, + reason: "无法建立 P2P 连接,使用服务器中转".to_string(), + }; + let _ = c.send(msg).await; + } + } + }); + } + SignalMessage::P2PReady { session_id, from_device, connected_addr, .. } => { + println!("★★★ P2P 连接就绪: session_id={}, from={}, addr={}", session_id, from_device, connected_addr); + tracing::info!("P2P 连接就绪: 对方设备={}, 地址={}", from_device, connected_addr); + // TODO: 切换到 P2P 传输 + } + SignalMessage::P2PFallback { session_id, from_device, reason, .. } => { + println!("★★★ P2P 连接失败,使用服务器中转: session_id={}, from={}, reason={}", session_id, from_device, reason); + tracing::info!("P2P 连接失败,继续使用服务器中转: {}", reason); + } _ => { tracing::debug!("收到信令消息: {:?}", msg); } @@ -818,7 +1249,7 @@ pub async fn remove_device( Ok(()) } -/// 连接到目标设备 +/// 连接到目标设备(保留用于兼容性,实际连接通过前端 WebSocket 实现) #[tauri::command] pub async fn connect_to_device( state: State<'_, AppStateHandle>, @@ -826,18 +1257,6 @@ pub async fn connect_to_device( ) -> Result<(), String> { let mut app_state = state.write().await; - app_state.connection_state = ConnectionState::Connecting; - - // TODO: 实现实际的P2P连接逻辑 - // 这里需要: - // 1. 连接到信令服务器 - // 2. 发送连接请求 - // 3. 进行ICE候选交换 - // 4. 建立P2P连接 - - // 模拟连接过程 - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - app_state.connection_state = ConnectionState::Connected { target_device_id: request.device_id.clone(), target_device_name: format!("设备 {}", request.device_id), @@ -847,6 +1266,1342 @@ pub async fn connect_to_device( Ok(()) } +/// 打开远程控制窗口 +#[tauri::command] +pub async fn open_remote_window( + app_handle: tauri::AppHandle, + device_id: String, + device_name: String, + server_url: String, +) -> Result<(), String> { + use tauri::Manager; + + println!("★★★ open_remote_window: device_id={}, device_name={}, server_url={}", device_id, device_name, server_url); + + // 移除 device_id 中的空格和横杠,生成安全的窗口标签 + let safe_id = device_id.chars().filter(|c| c.is_alphanumeric()).collect::(); + let window_label = format!("remote_{}", safe_id); + + // 检查窗口是否已存在 + if let Some(existing) = app_handle.get_window(&window_label) { + println!("★★★ 窗口已存在,聚焦"); + let _ = existing.set_focus(); + return Ok(()); + } + + // 构建远程控制页面的 HTML + let html_content = generate_remote_html(&device_id, &device_name, &server_url); + + println!("★★★ 创建新窗口..."); + + // 创建一个空白页面的窗口 + let window = match tauri::WindowBuilder::new( + &app_handle, + &window_label, + tauri::WindowUrl::App("about:blank".into()) + ) + .title(format!("远程控制 - {}", device_name)) + .inner_size(1280.0, 720.0) + .min_inner_size(800.0, 600.0) + .center() + .resizable(true) + .decorations(true) + .build() { + Ok(w) => w, + Err(e) => { + println!("★★★ 创建窗口失败: {}", e); + return Err(e.to_string()); + } + }; + + println!("★★★ 窗口创建成功,注入 HTML..."); + + // 使用 eval 注入 HTML 内容 + let escaped_html = html_content + .replace('\\', "\\\\") + .replace('`', "\\`") + .replace("${", "\\${"); + + let script = format!( + r#"document.open(); document.write(`{}`); document.close();"#, + escaped_html + ); + + // 延迟注入,确保窗口准备好 + let window_clone = window.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(500)); + if let Err(e) = window_clone.eval(&script) { + println!("★★★ 注入 HTML 失败: {}", e); + } else { + println!("★★★ HTML 注入成功"); + } + }); + + // 聚焦新窗口 + let _ = window.set_focus(); + + tracing::info!("打开远程控制窗口: device_id={}, device_name={}", device_id, device_name); + + Ok(()) +} + +/// 生成远程控制页面的 HTML (参考 UU 远程布局 - 优化版) +fn generate_remote_html(device_id: &str, device_name: &str, server_url: &str) -> String { + format!(r#" + + + + + 远程控制 - {device_name} + + + + +
+
+ +
+ + + +
+
+ + 连接中 +
+ 00:00:00 +
+ +
+ +
+ +
+ + + +
+
+ +
+ +
+ +
+ + + + + +
+
+

正在连接

+

正在连接到 {device_name}...

+

正在建立 WebSocket 连接...

+
+ + + +
+ + +
+
时长00:00:00
+
连接TCP relay
+
+
帧率-- fps
+
码率-- Mbps
+
延迟-- ms
+
帧延-- ms frm.
+
丢包0.0%
+
+
分辨率-- auto
+
+ + +
+ + + +
+ +
+ + + +"#, device_name = device_name, device_id = device_id, server_url = server_url) +} + /// 断开连接 #[tauri::command] pub async fn disconnect(state: State<'_, AppStateHandle>) -> Result<(), String> { @@ -1113,3 +2868,55 @@ X-GNOME-Autostart-enabled=true return Ok(()); } } + +/// 测试本地输入控制 - 将鼠标移动到指定位置并点击 +#[tauri::command] +pub async fn test_local_input() -> Result { + use easyremote_client_core::InputController; + + let mut results = Vec::new(); + results.push("=== 测试本地输入控制 ===".to_string()); + + // 测试创建 InputController + match InputController::new() { + Ok(mut controller) => { + results.push("✅ InputController 创建成功".to_string()); + + // 测试移动鼠标到屏幕中央 + let test_x = 960; + let test_y = 540; + + results.push(format!("正在移动鼠标到 ({}, {})...", test_x, test_y)); + + match controller.move_mouse(test_x, test_y) { + Ok(_) => { + results.push(format!("✅ 鼠标移动到 ({}, {}) 成功", test_x, test_y)); + } + Err(e) => { + results.push(format!("❌ 鼠标移动失败: {}", e)); + } + } + + // 等待一下 + std::thread::sleep(std::time::Duration::from_millis(500)); + + // 测试鼠标点击 + results.push("正在测试鼠标点击...".to_string()); + match controller.click(easyremote_client_core::MouseButton::Left) { + Ok(_) => { + results.push("✅ 鼠标点击成功".to_string()); + } + Err(e) => { + results.push(format!("❌ 鼠标点击失败: {}", e)); + } + } + } + Err(e) => { + results.push(format!("❌ 创建 InputController 失败: {}", e)); + } + } + + let result = results.join("\n"); + println!("{}", result); + Ok(result) +} diff --git a/crates/client-tauri/src/main.rs b/crates/client-tauri/src/main.rs index fd06d7d..a774729 100644 --- a/crates/client-tauri/src/main.rs +++ b/crates/client-tauri/src/main.rs @@ -176,10 +176,15 @@ fn main() { _ => {} }) .on_window_event(|event| { - // 关闭窗口时最小化到托盘而不是退出 + // 关闭窗口时的处理 if let WindowEvent::CloseRequested { api, .. } = event.event() { - event.window().hide().unwrap(); - api.prevent_close(); + let window = event.window(); + // 只有主窗口最小化到托盘,其他窗口(如远程控制窗口)直接关闭 + if window.label() == "main" { + window.hide().unwrap(); + api.prevent_close(); + } + // 远程控制窗口直接关闭(不阻止) } }) .setup(|app| { @@ -224,6 +229,7 @@ fn main() { commands::connect_to_device, commands::disconnect, commands::get_connection_state, + commands::open_remote_window, // 历史记录 commands::get_history, // 配置 @@ -233,6 +239,8 @@ fn main() { // 开机启动 commands::get_autostart, commands::set_autostart, + // 测试 + commands::test_local_input, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/crates/client-tauri/src/state.rs b/crates/client-tauri/src/state.rs index 46eb348..dab800b 100644 --- a/crates/client-tauri/src/state.rs +++ b/crates/client-tauri/src/state.rs @@ -4,6 +4,24 @@ use easyremote_common::types::{DeviceId, VerificationCode}; use easyremote_client_core::ClientConfig; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::RwLock; +use tokio::net::UdpSocket; + +/// P2P 连接信息 +#[derive(Debug, Clone)] +pub struct P2PConnection { + /// 会话 ID + pub session_id: String, + /// 对方设备 ID + pub peer_device_id: String, + /// 本地 UDP 端口 + pub local_port: u16, + /// 对方地址 + pub peer_addr: Option, + /// 是否已连接 + pub connected: bool, +} /// 应用状态 pub struct AppState { @@ -21,6 +39,10 @@ pub struct AppState { pub connection_state: ConnectionState, /// 当前会话ID pub current_session_id: Option, + /// P2P 连接(用于被控端接收直连数据) + pub p2p_socket: Option>, + /// P2P 连接信息 + pub p2p_connection: Option, } /// 当前用户信息 @@ -38,15 +60,20 @@ pub enum ConnectionState { Disconnected, /// 正在连接 Connecting, + /// 正在进行 P2P 握手 + P2PHandshaking { + target_device_id: String, + }, /// 已连接到设备 Connected { target_device_id: String, target_device_name: String, - connection_type: String, + connection_type: String, // "p2p" 或 "relay" }, /// 被控制中 BeingControlled { controller_device_id: String, + connection_type: String, // "p2p" 或 "relay" }, } @@ -75,6 +102,8 @@ impl AppState { auth_token, connection_state: ConnectionState::Disconnected, current_session_id: None, + p2p_socket: None, + p2p_connection: None, } } diff --git a/crates/client-tauri/tauri.conf.json b/crates/client-tauri/tauri.conf.json index 0c35531..747d839 100644 --- a/crates/client-tauri/tauri.conf.json +++ b/crates/client-tauri/tauri.conf.json @@ -83,6 +83,7 @@ }, "windows": [ { + "label": "main", "fullscreen": false, "height": 700, "resizable": true, diff --git a/crates/client-tauri/ui/src/App.vue b/crates/client-tauri/ui/src/App.vue index be6f1dd..7630955 100644 --- a/crates/client-tauri/ui/src/App.vue +++ b/crates/client-tauri/ui/src/App.vue @@ -1,6 +1,7 @@