mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 03:32:00 +08:00
feat(video): 事务化切换与前端统一编排,增强视频输入格式支持
- 后端:切换事务+transition_id,/stream/mode 返回 switching/transition_id 与实际 codec - 事件:新增 mode_switching/mode_ready,config/webrtc_ready/mode_changed 关联事务 - 编码/格式:扩展 NV21/NV16/NV24/RGB/BGR 输入与转换链路,RKMPP direct input 优化 - 前端:useVideoSession 统一切换,失败回退真实切回 MJPEG,菜单格式同步修复 - 清理:useVideoStream 降级为 MJPEG-only
This commit is contained in:
@@ -31,15 +31,14 @@ pub async fn apply_video_config(
|
||||
.format
|
||||
.as_ref()
|
||||
.and_then(|f| {
|
||||
serde_json::from_value::<crate::video::format::PixelFormat>(
|
||||
serde_json::Value::String(f.clone()),
|
||||
)
|
||||
serde_json::from_value::<crate::video::format::PixelFormat>(serde_json::Value::String(
|
||||
f.clone(),
|
||||
))
|
||||
.ok()
|
||||
})
|
||||
.unwrap_or(crate::video::format::PixelFormat::Mjpeg);
|
||||
|
||||
let resolution =
|
||||
crate::video::format::Resolution::new(new_config.width, new_config.height);
|
||||
let resolution = crate::video::format::Resolution::new(new_config.width, new_config.height);
|
||||
|
||||
// Step 1: 更新 WebRTC streamer 配置(停止现有 pipeline 和 sessions)
|
||||
state
|
||||
@@ -162,9 +161,16 @@ pub async fn apply_hid_config(
|
||||
// 如果描述符变更且当前使用 OTG 后端,需要重建 Gadget
|
||||
if descriptor_changed && new_config.backend == HidBackend::Otg {
|
||||
tracing::info!("OTG descriptor changed, updating gadget...");
|
||||
if let Err(e) = state.otg_service.update_descriptor(&new_config.otg_descriptor).await {
|
||||
if let Err(e) = state
|
||||
.otg_service
|
||||
.update_descriptor(&new_config.otg_descriptor)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to update OTG descriptor: {}", e);
|
||||
return Err(AppError::Config(format!("OTG descriptor update failed: {}", e)));
|
||||
return Err(AppError::Config(format!(
|
||||
"OTG descriptor update failed: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
tracing::info!("OTG descriptor updated successfully");
|
||||
}
|
||||
@@ -197,7 +203,10 @@ pub async fn apply_hid_config(
|
||||
.await
|
||||
.map_err(|e| AppError::Config(format!("HID reload failed: {}", e)))?;
|
||||
|
||||
tracing::info!("HID backend reloaded successfully: {:?}", new_config.backend);
|
||||
tracing::info!(
|
||||
"HID backend reloaded successfully: {:?}",
|
||||
new_config.backend
|
||||
);
|
||||
|
||||
// When switching to OTG backend, automatically enable MSD if not already enabled
|
||||
// OTG HID and MSD share the same USB gadget, so it makes sense to enable both
|
||||
@@ -245,7 +254,11 @@ pub async fn apply_msd_config(
|
||||
let old_msd_enabled = old_config.enabled;
|
||||
let new_msd_enabled = new_config.enabled;
|
||||
|
||||
tracing::info!("MSD enabled: old={}, new={}", old_msd_enabled, new_msd_enabled);
|
||||
tracing::info!(
|
||||
"MSD enabled: old={}, new={}",
|
||||
old_msd_enabled,
|
||||
new_msd_enabled
|
||||
);
|
||||
|
||||
if old_msd_enabled != new_msd_enabled {
|
||||
if new_msd_enabled {
|
||||
@@ -257,9 +270,9 @@ pub async fn apply_msd_config(
|
||||
&new_config.images_path,
|
||||
&new_config.drive_path,
|
||||
);
|
||||
msd.init().await.map_err(|e| {
|
||||
AppError::Config(format!("MSD initialization failed: {}", e))
|
||||
})?;
|
||||
msd.init()
|
||||
.await
|
||||
.map_err(|e| AppError::Config(format!("MSD initialization failed: {}", e)))?;
|
||||
|
||||
// Set event bus
|
||||
let events = state.events.clone();
|
||||
@@ -429,7 +442,10 @@ pub async fn apply_rustdesk_config(
|
||||
if let Err(e) = service.restart(new_config.clone()).await {
|
||||
tracing::error!("Failed to restart RustDesk service: {}", e);
|
||||
} else {
|
||||
tracing::info!("RustDesk service restarted with ID: {}", new_config.device_id);
|
||||
tracing::info!(
|
||||
"RustDesk service restarted with ID: {}",
|
||||
new_config.device_id
|
||||
);
|
||||
// Save generated keypair and UUID to config
|
||||
credentials_to_save = service.save_credentials();
|
||||
}
|
||||
|
||||
@@ -19,26 +19,26 @@
|
||||
pub(crate) mod apply;
|
||||
mod types;
|
||||
|
||||
pub(crate) mod video;
|
||||
mod stream;
|
||||
mod hid;
|
||||
mod msd;
|
||||
mod atx;
|
||||
mod audio;
|
||||
mod hid;
|
||||
mod msd;
|
||||
mod rustdesk;
|
||||
mod stream;
|
||||
pub(crate) mod video;
|
||||
mod web;
|
||||
|
||||
// 导出 handler 函数
|
||||
pub use video::{get_video_config, update_video_config};
|
||||
pub use stream::{get_stream_config, update_stream_config};
|
||||
pub use hid::{get_hid_config, update_hid_config};
|
||||
pub use msd::{get_msd_config, update_msd_config};
|
||||
pub use atx::{get_atx_config, update_atx_config};
|
||||
pub use audio::{get_audio_config, update_audio_config};
|
||||
pub use hid::{get_hid_config, update_hid_config};
|
||||
pub use msd::{get_msd_config, update_msd_config};
|
||||
pub use rustdesk::{
|
||||
get_rustdesk_config, get_rustdesk_status, update_rustdesk_config,
|
||||
regenerate_device_id, regenerate_device_password, get_device_password,
|
||||
get_device_password, get_rustdesk_config, get_rustdesk_status, regenerate_device_id,
|
||||
regenerate_device_password, update_rustdesk_config,
|
||||
};
|
||||
pub use stream::{get_stream_config, update_stream_config};
|
||||
pub use video::{get_video_config, update_video_config};
|
||||
pub use web::{get_web_config, update_web_config};
|
||||
|
||||
// 保留全局配置查询(向后兼容)
|
||||
|
||||
@@ -48,12 +48,16 @@ pub struct RustDeskStatusResponse {
|
||||
}
|
||||
|
||||
/// 获取 RustDesk 配置
|
||||
pub async fn get_rustdesk_config(State(state): State<Arc<AppState>>) -> Json<RustDeskConfigResponse> {
|
||||
pub async fn get_rustdesk_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Json<RustDeskConfigResponse> {
|
||||
Json(RustDeskConfigResponse::from(&state.config.get().rustdesk))
|
||||
}
|
||||
|
||||
/// 获取 RustDesk 完整状态(配置 + 服务状态)
|
||||
pub async fn get_rustdesk_status(State(state): State<Arc<AppState>>) -> Json<RustDeskStatusResponse> {
|
||||
pub async fn get_rustdesk_status(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Json<RustDeskStatusResponse> {
|
||||
let config = state.config.get().rustdesk.clone();
|
||||
|
||||
// 获取服务状态
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use serde::Deserialize;
|
||||
use typeshare::typeshare;
|
||||
use crate::config::*;
|
||||
use crate::error::AppError;
|
||||
use crate::rustdesk::config::RustDeskConfig;
|
||||
use crate::video::encoder::BitratePreset;
|
||||
use serde::Deserialize;
|
||||
use typeshare::typeshare;
|
||||
|
||||
// ===== Video Config =====
|
||||
#[typeshare]
|
||||
@@ -21,12 +21,16 @@ impl VideoConfigUpdate {
|
||||
pub fn validate(&self) -> crate::error::Result<()> {
|
||||
if let Some(width) = self.width {
|
||||
if !(320..=7680).contains(&width) {
|
||||
return Err(AppError::BadRequest("Invalid width: must be 320-7680".into()));
|
||||
return Err(AppError::BadRequest(
|
||||
"Invalid width: must be 320-7680".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(height) = self.height {
|
||||
if !(240..=4320).contains(&height) {
|
||||
return Err(AppError::BadRequest("Invalid height: must be 240-4320".into()));
|
||||
return Err(AppError::BadRequest(
|
||||
"Invalid height: must be 240-4320".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(fps) = self.fps {
|
||||
@@ -36,7 +40,9 @@ impl VideoConfigUpdate {
|
||||
}
|
||||
if let Some(quality) = self.quality {
|
||||
if !(1..=100).contains(&quality) {
|
||||
return Err(AppError::BadRequest("Invalid quality: must be 1-100".into()));
|
||||
return Err(AppError::BadRequest(
|
||||
"Invalid quality: must be 1-100".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -126,7 +132,8 @@ impl StreamConfigUpdate {
|
||||
if let Some(ref stun) = self.stun_server {
|
||||
if !stun.is_empty() && !stun.starts_with("stun:") {
|
||||
return Err(AppError::BadRequest(
|
||||
"STUN server must start with 'stun:' (e.g., stun:stun.l.google.com:19302)".into(),
|
||||
"STUN server must start with 'stun:' (e.g., stun:stun.l.google.com:19302)"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -153,16 +160,32 @@ impl StreamConfigUpdate {
|
||||
}
|
||||
// STUN/TURN settings - empty string means clear (use public servers), Some("value") means set custom
|
||||
if let Some(ref stun) = self.stun_server {
|
||||
config.stun_server = if stun.is_empty() { None } else { Some(stun.clone()) };
|
||||
config.stun_server = if stun.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(stun.clone())
|
||||
};
|
||||
}
|
||||
if let Some(ref turn) = self.turn_server {
|
||||
config.turn_server = if turn.is_empty() { None } else { Some(turn.clone()) };
|
||||
config.turn_server = if turn.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(turn.clone())
|
||||
};
|
||||
}
|
||||
if let Some(ref username) = self.turn_username {
|
||||
config.turn_username = if username.is_empty() { None } else { Some(username.clone()) };
|
||||
config.turn_username = if username.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(username.clone())
|
||||
};
|
||||
}
|
||||
if let Some(ref password) = self.turn_password {
|
||||
config.turn_password = if password.is_empty() { None } else { Some(password.clone()) };
|
||||
config.turn_password = if password.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(password.clone())
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,19 +208,25 @@ impl OtgDescriptorConfigUpdate {
|
||||
// Validate manufacturer string length
|
||||
if let Some(ref s) = self.manufacturer {
|
||||
if s.len() > 126 {
|
||||
return Err(AppError::BadRequest("Manufacturer string too long (max 126 chars)".into()));
|
||||
return Err(AppError::BadRequest(
|
||||
"Manufacturer string too long (max 126 chars)".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
// Validate product string length
|
||||
if let Some(ref s) = self.product {
|
||||
if s.len() > 126 {
|
||||
return Err(AppError::BadRequest("Product string too long (max 126 chars)".into()));
|
||||
return Err(AppError::BadRequest(
|
||||
"Product string too long (max 126 chars)".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
// Validate serial number string length
|
||||
if let Some(ref s) = self.serial_number {
|
||||
if s.len() > 126 {
|
||||
return Err(AppError::BadRequest("Serial number string too long (max 126 chars)".into()));
|
||||
return Err(AppError::BadRequest(
|
||||
"Serial number string too long (max 126 chars)".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -469,7 +498,8 @@ impl RustDeskConfigUpdate {
|
||||
if let Some(ref server) = self.rendezvous_server {
|
||||
if !server.is_empty() && !server.contains(':') {
|
||||
return Err(AppError::BadRequest(
|
||||
"Rendezvous server must be in format 'host:port' (e.g., rs.example.com:21116)".into(),
|
||||
"Rendezvous server must be in format 'host:port' (e.g., rs.example.com:21116)"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -477,7 +507,8 @@ impl RustDeskConfigUpdate {
|
||||
if let Some(ref server) = self.relay_server {
|
||||
if !server.is_empty() && !server.contains(':') {
|
||||
return Err(AppError::BadRequest(
|
||||
"Relay server must be in format 'host:port' (e.g., rs.example.com:21117)".into(),
|
||||
"Relay server must be in format 'host:port' (e.g., rs.example.com:21117)"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -500,10 +531,18 @@ impl RustDeskConfigUpdate {
|
||||
config.rendezvous_server = server.clone();
|
||||
}
|
||||
if let Some(ref server) = self.relay_server {
|
||||
config.relay_server = if server.is_empty() { None } else { Some(server.clone()) };
|
||||
config.relay_server = if server.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(server.clone())
|
||||
};
|
||||
}
|
||||
if let Some(ref key) = self.relay_key {
|
||||
config.relay_key = if key.is_empty() { None } else { Some(key.clone()) };
|
||||
config.relay_key = if key.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(key.clone())
|
||||
};
|
||||
}
|
||||
if let Some(ref password) = self.device_password {
|
||||
if !password.is_empty() {
|
||||
|
||||
Reference in New Issue
Block a user