mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-17 16:36:44 +08:00
fix: 优化 WebRTC 建连流程、修复平台信息、修复虚拟键盘键值映射
- WebRTC:默认 mDNS 调整为 QueryOnly,Answer 阶段改为等待 ICE gathering complete(2.5s 超时),提升首次建连成功率与候选完整性 - WebRTC:前端建连流程增加阶段化状态与串行保护(connectInFlight/ready gate),优化配置变更后的重连时机与失败处理,减少竞态和无效重试 - Device:平台信息补充 `/proc/device-tree/model` 回退并统一展示为“处理器/平台” - HID:键盘输入链路统一为 HID usage + modifier bitmask,修复虚拟键盘/宏/粘贴键值映射错误
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
//!
|
||||
//! Keyboard event (type 0x01):
|
||||
//! - Byte 1: Event type (0x00 = down, 0x01 = up)
|
||||
//! - Byte 2: Key code (USB HID usage code or JS keyCode)
|
||||
//! - Byte 2: Key code (USB HID usage code)
|
||||
//! - Byte 3: Modifiers bitmask
|
||||
//! - Bit 0: Left Ctrl
|
||||
//! - Bit 1: Left Shift
|
||||
@@ -119,7 +119,7 @@ fn parse_keyboard_message(data: &[u8]) -> Option<HidChannelEvent> {
|
||||
event_type,
|
||||
key,
|
||||
modifiers,
|
||||
is_usb_hid: false, // WebRTC datachannel sends JS keycodes
|
||||
is_usb_hid: true, // WebRTC/WebSocket HID channel sends USB HID usages
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -245,6 +245,7 @@ mod tests {
|
||||
assert_eq!(kb.key, 0x04);
|
||||
assert!(kb.modifiers.left_ctrl);
|
||||
assert!(!kb.modifiers.left_shift);
|
||||
assert!(kb.is_usb_hid);
|
||||
}
|
||||
_ => panic!("Expected keyboard event"),
|
||||
}
|
||||
@@ -280,7 +281,7 @@ mod tests {
|
||||
right_alt: false,
|
||||
right_meta: false,
|
||||
},
|
||||
is_usb_hid: false,
|
||||
is_usb_hid: true,
|
||||
};
|
||||
|
||||
let encoded = encode_keyboard_event(&event);
|
||||
|
||||
@@ -183,31 +183,59 @@ fn get_hostname() -> String {
|
||||
.unwrap_or_else(|_| "unknown".to_string())
|
||||
}
|
||||
|
||||
/// Get CPU model name from /proc/cpuinfo
|
||||
/// Get CPU model name from /proc/cpuinfo, fallback to device-tree model
|
||||
fn get_cpu_model() -> String {
|
||||
std::fs::read_to_string("/proc/cpuinfo")
|
||||
let cpuinfo = std::fs::read_to_string("/proc/cpuinfo").ok();
|
||||
|
||||
if let Some(model) = cpuinfo
|
||||
.as_deref()
|
||||
.and_then(parse_cpu_model_from_cpuinfo_content)
|
||||
{
|
||||
return model;
|
||||
}
|
||||
|
||||
if let Some(model) = read_device_tree_model() {
|
||||
return model;
|
||||
}
|
||||
|
||||
if let Some(content) = cpuinfo.as_deref() {
|
||||
let cores = content
|
||||
.lines()
|
||||
.filter(|line| line.starts_with("processor"))
|
||||
.count();
|
||||
if cores > 0 {
|
||||
return format!("{} {}C", std::env::consts::ARCH, cores);
|
||||
}
|
||||
}
|
||||
|
||||
std::env::consts::ARCH.to_string()
|
||||
}
|
||||
|
||||
fn parse_cpu_model_from_cpuinfo_content(content: &str) -> Option<String> {
|
||||
content
|
||||
.lines()
|
||||
.find(|line| line.starts_with("model name") || line.starts_with("Model"))
|
||||
.and_then(|line| line.split(':').nth(1))
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
fn read_device_tree_model() -> Option<String> {
|
||||
std::fs::read("/proc/device-tree/model")
|
||||
.ok()
|
||||
.and_then(|content| {
|
||||
// Try to get model name
|
||||
let model = content
|
||||
.lines()
|
||||
.find(|line| line.starts_with("model name") || line.starts_with("Model"))
|
||||
.and_then(|line| line.split(':').nth(1))
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
.and_then(|bytes| parse_device_tree_model_bytes(&bytes))
|
||||
}
|
||||
|
||||
if model.is_some() {
|
||||
return model;
|
||||
}
|
||||
fn parse_device_tree_model_bytes(bytes: &[u8]) -> Option<String> {
|
||||
let model = String::from_utf8_lossy(bytes)
|
||||
.trim_matches(|c: char| c == '\0' || c.is_whitespace())
|
||||
.to_string();
|
||||
|
||||
// Fallback: show arch and core count
|
||||
let cores = content
|
||||
.lines()
|
||||
.filter(|line| line.starts_with("processor"))
|
||||
.count();
|
||||
Some(format!("{} {}C", std::env::consts::ARCH, cores))
|
||||
})
|
||||
.unwrap_or_else(|| std::env::consts::ARCH.to_string())
|
||||
if model.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(model)
|
||||
}
|
||||
}
|
||||
|
||||
/// CPU usage state for calculating usage between samples
|
||||
@@ -389,6 +417,38 @@ fn get_network_addresses() -> Vec<NetworkAddress> {
|
||||
addresses
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse_cpu_model_from_cpuinfo_content, parse_device_tree_model_bytes};
|
||||
|
||||
#[test]
|
||||
fn parse_cpu_model_from_model_name_field() {
|
||||
let input = "processor\t: 0\nmodel name\t: Intel(R) Xeon(R)\n";
|
||||
assert_eq!(
|
||||
parse_cpu_model_from_cpuinfo_content(input),
|
||||
Some("Intel(R) Xeon(R)".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_cpu_model_from_model_field() {
|
||||
let input = "processor\t: 0\nModel\t\t: Raspberry Pi 4 Model B Rev 1.4\n";
|
||||
assert_eq!(
|
||||
parse_cpu_model_from_cpuinfo_content(input),
|
||||
Some("Raspberry Pi 4 Model B Rev 1.4".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_device_tree_model_trimmed() {
|
||||
let input = b"Onething OEC Box\0\n";
|
||||
assert_eq!(
|
||||
parse_device_tree_model_bytes(input),
|
||||
Some("Onething OEC Box".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Authentication
|
||||
// ============================================================================
|
||||
@@ -2053,10 +2113,11 @@ pub async fn webrtc_offer(
|
||||
));
|
||||
}
|
||||
|
||||
// Create session if client_id not provided
|
||||
// Backward compatibility: `client_id` is treated as an existing session_id hint.
|
||||
// New clients should not pass it; each offer creates a fresh session.
|
||||
let webrtc = state.stream_manager.webrtc_streamer();
|
||||
let session_id = if let Some(client_id) = &req.client_id {
|
||||
// Check if session exists
|
||||
// Reuse only when it matches an active session ID.
|
||||
if webrtc.get_session(client_id).await.is_some() {
|
||||
client_id.clone()
|
||||
} else {
|
||||
@@ -2411,15 +2472,13 @@ pub async fn hid_otg_self_check(State(state): State<Arc<AppState>>) -> Json<OtgS
|
||||
let hid_backend_is_otg = matches!(config.hid.backend, crate::config::HidBackend::Otg);
|
||||
let mut checks = Vec::new();
|
||||
|
||||
let build_response = |
|
||||
checks: Vec<OtgSelfCheckItem>,
|
||||
selected_udc: Option<String>,
|
||||
bound_udc: Option<String>,
|
||||
udc_state: Option<String>,
|
||||
udc_speed: Option<String>,
|
||||
available_udcs: Vec<String>,
|
||||
other_gadgets: Vec<String>,
|
||||
| {
|
||||
let build_response = |checks: Vec<OtgSelfCheckItem>,
|
||||
selected_udc: Option<String>,
|
||||
bound_udc: Option<String>,
|
||||
udc_state: Option<String>,
|
||||
udc_speed: Option<String>,
|
||||
available_udcs: Vec<String>,
|
||||
other_gadgets: Vec<String>| {
|
||||
let error_count = checks
|
||||
.iter()
|
||||
.filter(|item| item.level == OtgSelfCheckLevel::Error)
|
||||
@@ -2528,7 +2587,9 @@ pub async fn hid_otg_self_check(State(state): State<Arc<AppState>>) -> Json<OtgS
|
||||
OtgSelfCheckLevel::Info
|
||||
},
|
||||
"Check configured UDC validity",
|
||||
Some("You can set hid_otg_udc in settings to avoid ambiguity in multi-controller setups"),
|
||||
Some(
|
||||
"You can set hid_otg_udc in settings to avoid ambiguity in multi-controller setups",
|
||||
),
|
||||
Some("/sys/class/udc"),
|
||||
);
|
||||
}
|
||||
@@ -2854,7 +2915,6 @@ pub async fn hid_otg_self_check(State(state): State<Arc<AppState>>) -> Json<OtgS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if !other_gadgets.is_empty() {
|
||||
|
||||
@@ -18,7 +18,9 @@ pub fn mdns_mode_from_env() -> Option<MulticastDnsMode> {
|
||||
}
|
||||
|
||||
pub fn mdns_mode() -> MulticastDnsMode {
|
||||
mdns_mode_from_env().unwrap_or(MulticastDnsMode::QueryAndGather)
|
||||
// Default to QueryOnly to avoid gathering .local host candidates by default.
|
||||
// This is generally more stable for LAN first-connection while preserving mDNS queries.
|
||||
mdns_mode_from_env().unwrap_or(MulticastDnsMode::QueryOnly)
|
||||
}
|
||||
|
||||
pub fn mdns_mode_label(mode: MulticastDnsMode) -> &'static str {
|
||||
|
||||
@@ -317,14 +317,26 @@ impl PeerConnection {
|
||||
.await
|
||||
.map_err(|e| AppError::VideoError(format!("Failed to create answer: {}", e)))?;
|
||||
|
||||
// Wait for ICE gathering complete (or timeout) after setting local description.
|
||||
// This improves first-connection robustness by returning a fuller initial candidate set.
|
||||
let mut gather_complete = self.pc.gathering_complete_promise().await;
|
||||
|
||||
// Set local description
|
||||
self.pc
|
||||
.set_local_description(answer.clone())
|
||||
.await
|
||||
.map_err(|e| AppError::VideoError(format!("Failed to set local description: {}", e)))?;
|
||||
|
||||
// Wait a bit for ICE candidates to gather
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
const ICE_GATHER_TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_millis(2500);
|
||||
if tokio::time::timeout(ICE_GATHER_TIMEOUT, gather_complete.recv())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
debug!(
|
||||
"ICE gathering timeout after {:?} for session {}",
|
||||
ICE_GATHER_TIMEOUT, self.session_id
|
||||
);
|
||||
}
|
||||
|
||||
// Get gathered ICE candidates
|
||||
let candidates = self.ice_candidates.lock().await.clone();
|
||||
|
||||
@@ -833,13 +833,24 @@ impl UniversalSession {
|
||||
}
|
||||
}
|
||||
|
||||
let mut gather_complete = self.pc.gathering_complete_promise().await;
|
||||
|
||||
self.pc
|
||||
.set_local_description(answer.clone())
|
||||
.await
|
||||
.map_err(|e| AppError::VideoError(format!("Failed to set local description: {}", e)))?;
|
||||
|
||||
// Wait for ICE candidates
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
// Wait for ICE gathering complete (or timeout) to return a fuller initial candidate set.
|
||||
const ICE_GATHER_TIMEOUT: Duration = Duration::from_millis(2500);
|
||||
if tokio::time::timeout(ICE_GATHER_TIMEOUT, gather_complete.recv())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
debug!(
|
||||
"ICE gathering timeout after {:?} for session {}",
|
||||
ICE_GATHER_TIMEOUT, self.session_id
|
||||
);
|
||||
}
|
||||
|
||||
let candidates = self.ice_candidates.lock().await.clone();
|
||||
Ok(SdpAnswer::with_candidates(answer.sdp, candidates))
|
||||
|
||||
Reference in New Issue
Block a user