feat: 完善架构优化性能

- 调整音视频架构,提升 RKMPP 编码 MJPEG-->H264 性能,同时解决丢帧马赛克问题;
- 删除多用户逻辑,只保留单用户,支持设置 web 单会话;
- 修复删除体验不好的的回退逻辑,前端页面菜单位置微调;
- 增加 OTG USB 设备动态调整功能;
- 修复 mdns 问题,webrtc 视频切换更顺畅。
This commit is contained in:
mofeng
2026-01-25 16:04:29 +08:00
parent 01e01430da
commit 1786b7689d
66 changed files with 4225 additions and 2936 deletions

View File

@@ -6,6 +6,7 @@ use std::sync::Arc;
use crate::config::*;
use crate::error::{AppError, Result};
use crate::events::SystemEvent;
use crate::state::AppState;
/// 应用 Video 配置变更
@@ -57,27 +58,55 @@ pub async fn apply_video_config(
.map_err(|e| AppError::VideoError(format!("Failed to apply video config: {}", e)))?;
tracing::info!("Video config applied to streamer");
// Step 3: 重启 streamer
if let Err(e) = state.stream_manager.start().await {
tracing::error!("Failed to start streamer after config change: {}", e);
} else {
tracing::info!("Streamer started after config change");
// Step 3: 重启 streamer(仅 MJPEG 模式)
if !state.stream_manager.is_webrtc_enabled().await {
if let Err(e) = state.stream_manager.start().await {
tracing::error!("Failed to start streamer after config change: {}", e);
} else {
tracing::info!("Streamer started after config change");
}
}
// Step 4: 更新 WebRTC frame source
if let Some(frame_tx) = state.stream_manager.frame_sender().await {
let receiver_count = frame_tx.receiver_count();
// 配置 WebRTC direct capture所有模式统一配置
let (device_path, _resolution, _format, _fps, jpeg_quality) = state
.stream_manager
.streamer()
.current_capture_config()
.await;
if let Some(device_path) = device_path {
state
.stream_manager
.webrtc_streamer()
.set_video_source(frame_tx)
.set_capture_device(device_path, jpeg_quality)
.await;
tracing::info!(
"WebRTC streamer frame source updated (receiver_count={})",
receiver_count
);
} else {
tracing::warn!("No frame source available after config change");
tracing::warn!("No capture device configured for WebRTC");
}
if state.stream_manager.is_webrtc_enabled().await {
use crate::video::encoder::VideoCodecType;
let codec = state
.stream_manager
.webrtc_streamer()
.current_video_codec()
.await;
let codec_str = match codec {
VideoCodecType::H264 => "h264",
VideoCodecType::H265 => "h265",
VideoCodecType::VP8 => "vp8",
VideoCodecType::VP9 => "vp9",
}
.to_string();
let is_hardware = state
.stream_manager
.webrtc_streamer()
.is_hardware_encoding()
.await;
state.events.publish(SystemEvent::WebRTCReady {
transition_id: None,
codec: codec_str,
hardware: is_hardware,
});
}
tracing::info!("Video config applied successfully");
@@ -157,6 +186,15 @@ pub async fn apply_hid_config(
) -> Result<()> {
// 检查 OTG 描述符是否变更
let descriptor_changed = old_config.otg_descriptor != new_config.otg_descriptor;
let old_hid_functions = old_config.effective_otg_functions();
let new_hid_functions = new_config.effective_otg_functions();
let hid_functions_changed = old_hid_functions != new_hid_functions;
if new_config.backend == HidBackend::Otg && new_hid_functions.is_empty() {
return Err(AppError::BadRequest(
"OTG HID functions cannot be empty".to_string(),
));
}
// 如果描述符变更且当前使用 OTG 后端,需要重建 Gadget
if descriptor_changed && new_config.backend == HidBackend::Otg {
@@ -181,6 +219,7 @@ pub async fn apply_hid_config(
&& old_config.ch9329_baudrate == new_config.ch9329_baudrate
&& old_config.otg_udc == new_config.otg_udc
&& !descriptor_changed
&& !hid_functions_changed
{
tracing::info!("HID config unchanged, skipping reload");
return Ok(());
@@ -188,6 +227,16 @@ pub async fn apply_hid_config(
tracing::info!("Applying HID config changes...");
if new_config.backend == HidBackend::Otg
&& (hid_functions_changed || old_config.backend != HidBackend::Otg)
{
state
.otg_service
.update_hid_functions(new_hid_functions.clone())
.await
.map_err(|e| AppError::Config(format!("OTG HID function update failed: {}", e)))?;
}
let new_hid_backend = match new_config.backend {
HidBackend::Otg => crate::hid::HidBackendType::Otg,
HidBackend::Ch9329 => crate::hid::HidBackendType::Ch9329 {
@@ -208,32 +257,6 @@ pub async fn apply_hid_config(
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
if new_config.backend == HidBackend::Otg && old_config.backend != HidBackend::Otg {
let msd_guard = state.msd.read().await;
if msd_guard.is_none() {
drop(msd_guard); // Release read lock before acquiring write lock
tracing::info!("OTG HID enabled, automatically initializing MSD...");
// Get MSD config from store
let config = state.config.get();
let msd =
crate::msd::MsdController::new(state.otg_service.clone(), config.msd.msd_dir_path());
if let Err(e) = msd.init().await {
tracing::warn!("Failed to auto-initialize MSD for OTG: {}", e);
} else {
let events = state.events.clone();
msd.set_event_bus(events).await;
*state.msd.write().await = Some(msd);
tracing::info!("MSD automatically initialized for OTG mode");
}
}
}
Ok(())
}

View File

@@ -0,0 +1,33 @@
use axum::{extract::State, Json};
use std::sync::Arc;
use crate::config::AuthConfig;
use crate::error::Result;
use crate::state::AppState;
use super::types::AuthConfigUpdate;
/// Get auth configuration (sensitive fields are cleared)
pub async fn get_auth_config(State(state): State<Arc<AppState>>) -> Json<AuthConfig> {
let mut auth = state.config.get().auth.clone();
auth.totp_secret = None;
Json(auth)
}
/// Update auth configuration
pub async fn update_auth_config(
State(state): State<Arc<AppState>>,
Json(update): Json<AuthConfigUpdate>,
) -> Result<Json<AuthConfig>> {
update.validate()?;
state
.config
.update(|config| {
update.apply_to(&mut config.auth);
})
.await?;
let mut auth = state.config.get().auth.clone();
auth.totp_secret = None;
Ok(Json(auth))
}

View File

@@ -21,6 +21,7 @@ mod types;
mod atx;
mod audio;
mod auth;
mod hid;
mod msd;
mod rustdesk;
@@ -31,6 +32,7 @@ mod web;
// 导出 handler 函数
pub use atx::{get_atx_config, update_atx_config};
pub use audio::{get_audio_config, update_audio_config};
pub use auth::{get_auth_config, update_auth_config};
pub use hid::{get_hid_config, update_hid_config};
pub use msd::{get_msd_config, update_msd_config};
pub use rustdesk::{

View File

@@ -6,6 +6,25 @@ use serde::Deserialize;
use std::path::Path;
use typeshare::typeshare;
// ===== Auth Config =====
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct AuthConfigUpdate {
pub single_user_allow_multiple_sessions: Option<bool>,
}
impl AuthConfigUpdate {
pub fn validate(&self) -> crate::error::Result<()> {
Ok(())
}
pub fn apply_to(&self, config: &mut AuthConfig) {
if let Some(allow_multiple) = self.single_user_allow_multiple_sessions {
config.single_user_allow_multiple_sessions = allow_multiple;
}
}
}
// ===== Video Config =====
#[typeshare]
#[derive(Debug, Deserialize)]
@@ -252,6 +271,32 @@ impl OtgDescriptorConfigUpdate {
}
}
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct OtgHidFunctionsUpdate {
pub keyboard: Option<bool>,
pub mouse_relative: Option<bool>,
pub mouse_absolute: Option<bool>,
pub consumer: Option<bool>,
}
impl OtgHidFunctionsUpdate {
pub fn apply_to(&self, config: &mut OtgHidFunctions) {
if let Some(enabled) = self.keyboard {
config.keyboard = enabled;
}
if let Some(enabled) = self.mouse_relative {
config.mouse_relative = enabled;
}
if let Some(enabled) = self.mouse_absolute {
config.mouse_absolute = enabled;
}
if let Some(enabled) = self.consumer {
config.consumer = enabled;
}
}
}
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct HidConfigUpdate {
@@ -260,6 +305,8 @@ pub struct HidConfigUpdate {
pub ch9329_baudrate: Option<u32>,
pub otg_udc: Option<String>,
pub otg_descriptor: Option<OtgDescriptorConfigUpdate>,
pub otg_profile: Option<OtgHidProfile>,
pub otg_functions: Option<OtgHidFunctionsUpdate>,
pub mouse_absolute: Option<bool>,
}
@@ -295,6 +342,12 @@ impl HidConfigUpdate {
if let Some(ref desc) = self.otg_descriptor {
desc.apply_to(&mut config.otg_descriptor);
}
if let Some(profile) = self.otg_profile.clone() {
config.otg_profile = profile;
}
if let Some(ref functions) = self.otg_functions {
functions.apply_to(&mut config.otg_functions);
}
if let Some(absolute) = self.mouse_absolute {
config.mouse_absolute = absolute;
}

View File

@@ -12,6 +12,7 @@ use tracing::{info, warn};
use crate::auth::{Session, SESSION_COOKIE};
use crate::config::{AppConfig, StreamMode};
use crate::error::{AppError, Result};
use crate::events::SystemEvent;
use crate::state::AppState;
use crate::video::encoder::BitratePreset;
@@ -407,6 +408,13 @@ pub async fn login(
.await?
.ok_or_else(|| AppError::AuthError("Invalid username or password".to_string()))?;
if !config.auth.single_user_allow_multiple_sessions {
// Kick existing sessions before creating a new one.
let revoked_ids = state.sessions.list_ids().await?;
state.sessions.delete_all().await?;
state.remember_revoked_sessions(revoked_ids).await;
}
// Create session
let session = state.sessions.create(&user.id).await?;
@@ -465,15 +473,15 @@ pub async fn auth_check(
axum::Extension(session): axum::Extension<Session>,
) -> Json<AuthCheckResponse> {
// Get user info from user_id
let (username, is_admin) = match state.users.get(&session.user_id).await {
Ok(Some(user)) => (Some(user.username), user.is_admin),
_ => (Some(session.user_id.clone()), false), // Fallback to user_id if user not found
let username = match state.users.get(&session.user_id).await {
Ok(Some(user)) => Some(user.username),
_ => Some(session.user_id.clone()), // Fallback to user_id if user not found
};
Json(AuthCheckResponse {
authenticated: true,
user: username,
is_admin,
is_admin: true,
})
}
@@ -797,34 +805,57 @@ pub async fn update_config(
}
tracing::info!("Video config applied successfully");
// Step 3: Start the streamer to begin capturing frames
// This is necessary because apply_video_config only creates the capturer but doesn't start it
if let Err(e) = state.stream_manager.start().await {
tracing::error!("Failed to start streamer after config change: {}", e);
// Don't fail the request - the stream might start later when client connects
} else {
tracing::info!("Streamer started after config change");
// Step 3: Start the streamer to begin capturing frames (MJPEG mode only)
if !state.stream_manager.is_webrtc_enabled().await {
// This is necessary because apply_video_config only creates the capturer but doesn't start it
if let Err(e) = state.stream_manager.start().await {
tracing::error!("Failed to start streamer after config change: {}", e);
// Don't fail the request - the stream might start later when client connects
} else {
tracing::info!("Streamer started after config change");
}
}
// Update frame source from the NEW capturer
// This is critical - the old frame_tx is invalid after config change
// New sessions will use this frame_tx when they connect
if let Some(frame_tx) = state.stream_manager.frame_sender().await {
let receiver_count = frame_tx.receiver_count();
// Use WebRtcStreamer (new unified interface)
// Configure WebRTC direct capture (all modes)
let (device_path, _resolution, _format, _fps, jpeg_quality) = state
.stream_manager
.streamer()
.current_capture_config()
.await;
if let Some(device_path) = device_path {
state
.stream_manager
.webrtc_streamer()
.set_video_source(frame_tx)
.set_capture_device(device_path, jpeg_quality)
.await;
tracing::info!(
"WebRTC streamer frame source updated with new capturer (receiver_count={})",
receiver_count
);
} else {
tracing::warn!(
"No frame source available after config change - streamer may not be running"
);
tracing::warn!("No capture device configured for WebRTC");
}
if state.stream_manager.is_webrtc_enabled().await {
use crate::video::encoder::VideoCodecType;
let codec = state
.stream_manager
.webrtc_streamer()
.current_video_codec()
.await;
let codec_str = match codec {
VideoCodecType::H264 => "h264",
VideoCodecType::H265 => "h265",
VideoCodecType::VP8 => "vp8",
VideoCodecType::VP9 => "vp9",
}
.to_string();
let is_hardware = state
.stream_manager
.webrtc_streamer()
.is_hardware_encoding()
.await;
state.events.publish(SystemEvent::WebRTCReady {
transition_id: None,
codec: codec_str,
hardware: is_hardware,
});
}
}
@@ -1388,8 +1419,9 @@ pub async fn stream_mode_set(
}
};
let no_switch_needed = !tx.accepted && !tx.switching && tx.transition_id.is_none();
Ok(Json(StreamModeResponse {
success: tx.accepted,
success: tx.accepted || no_switch_needed,
mode: if tx.accepted {
requested_mode_str.to_string()
} else {
@@ -1935,6 +1967,7 @@ pub async fn webrtc_close_session(
#[derive(Serialize)]
pub struct IceServersResponse {
pub ice_servers: Vec<IceServerInfo>,
pub mdns_mode: String,
}
#[derive(Serialize)]
@@ -1950,6 +1983,7 @@ pub struct IceServerInfo {
/// Returns user-configured servers, or Google STUN as fallback if none configured
pub async fn webrtc_ice_servers(State(state): State<Arc<AppState>>) -> Json<IceServersResponse> {
use crate::webrtc::config::public_ice;
use crate::webrtc::mdns::{mdns_mode, mdns_mode_label};
let config = state.config.get();
let mut ice_servers = Vec::new();
@@ -2005,7 +2039,13 @@ pub async fn webrtc_ice_servers(State(state): State<Arc<AppState>>) -> Json<IceS
// Note: TURN servers are not provided - users must configure their own
}
Json(IceServersResponse { ice_servers })
let mdns_mode = mdns_mode();
let mdns_mode = mdns_mode_label(mdns_mode).to_string();
Json(IceServersResponse {
ice_servers,
mdns_mode,
})
}
// ============================================================================
@@ -2661,200 +2701,9 @@ pub async fn list_audio_devices(
}
// ============================================================================
// User Management
// Password Management
// ============================================================================
use axum::extract::Path;
use axum::Extension;
/// User response (without password hash)
#[derive(Serialize)]
pub struct UserResponse {
pub id: String,
pub username: String,
pub is_admin: bool,
pub created_at: String,
pub updated_at: String,
}
impl From<crate::auth::User> for UserResponse {
fn from(user: crate::auth::User) -> Self {
Self {
id: user.id,
username: user.username,
is_admin: user.is_admin,
created_at: user.created_at.to_rfc3339(),
updated_at: user.updated_at.to_rfc3339(),
}
}
}
/// List all users (admin only)
pub async fn list_users(
State(state): State<Arc<AppState>>,
Extension(session): Extension<Session>,
) -> Result<Json<Vec<UserResponse>>> {
// Check if current user is admin
let current_user = state
.users
.get(&session.user_id)
.await?
.ok_or_else(|| AppError::AuthError("User not found".to_string()))?;
if !current_user.is_admin {
return Err(AppError::Forbidden("Admin access required".to_string()));
}
let users = state.users.list().await?;
let response: Vec<UserResponse> = users.into_iter().map(UserResponse::from).collect();
Ok(Json(response))
}
/// Create user request
#[derive(Deserialize)]
pub struct CreateUserRequest {
pub username: String,
pub password: String,
pub is_admin: bool,
}
/// Create new user (admin only)
pub async fn create_user(
State(state): State<Arc<AppState>>,
Extension(session): Extension<Session>,
Json(req): Json<CreateUserRequest>,
) -> Result<Json<UserResponse>> {
// Check if current user is admin
let current_user = state
.users
.get(&session.user_id)
.await?
.ok_or_else(|| AppError::AuthError("User not found".to_string()))?;
if !current_user.is_admin {
return Err(AppError::Forbidden("Admin access required".to_string()));
}
// Validate input
if req.username.len() < 2 {
return Err(AppError::BadRequest(
"Username must be at least 2 characters".to_string(),
));
}
if req.password.len() < 4 {
return Err(AppError::BadRequest(
"Password must be at least 4 characters".to_string(),
));
}
let user = state
.users
.create(&req.username, &req.password, req.is_admin)
.await?;
info!("User created: {} (admin: {})", user.username, user.is_admin);
Ok(Json(UserResponse::from(user)))
}
/// Update user request
#[derive(Deserialize)]
pub struct UpdateUserRequest {
pub username: Option<String>,
pub is_admin: Option<bool>,
}
/// Update user (admin only)
pub async fn update_user(
State(state): State<Arc<AppState>>,
Extension(session): Extension<Session>,
Path(user_id): Path<String>,
Json(req): Json<UpdateUserRequest>,
) -> Result<Json<UserResponse>> {
// Check if current user is admin
let current_user = state
.users
.get(&session.user_id)
.await?
.ok_or_else(|| AppError::AuthError("User not found".to_string()))?;
if !current_user.is_admin {
return Err(AppError::Forbidden("Admin access required".to_string()));
}
// Get target user
let mut user = state
.users
.get(&user_id)
.await?
.ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
// Update fields if provided
if let Some(username) = req.username {
if username.len() < 2 {
return Err(AppError::BadRequest(
"Username must be at least 2 characters".to_string(),
));
}
user.username = username;
}
if let Some(is_admin) = req.is_admin {
user.is_admin = is_admin;
}
// Note: We need to add an update method to UserStore
// For now, return error
Err(AppError::Internal(
"User update not yet implemented".to_string(),
))
}
/// Delete user (admin only)
pub async fn delete_user(
State(state): State<Arc<AppState>>,
Extension(session): Extension<Session>,
Path(user_id): Path<String>,
) -> Result<Json<LoginResponse>> {
// Check if current user is admin
let current_user = state
.users
.get(&session.user_id)
.await?
.ok_or_else(|| AppError::AuthError("User not found".to_string()))?;
if !current_user.is_admin {
return Err(AppError::Forbidden("Admin access required".to_string()));
}
// Prevent deleting self
if user_id == session.user_id {
return Err(AppError::BadRequest(
"Cannot delete your own account".to_string(),
));
}
// Check if this is the last admin
let users = state.users.list().await?;
let admin_count = users.iter().filter(|u| u.is_admin).count();
let target_user = state
.users
.get(&user_id)
.await?
.ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
if target_user.is_admin && admin_count <= 1 {
return Err(AppError::BadRequest(
"Cannot delete the last admin user".to_string(),
));
}
state.users.delete(&user_id).await?;
info!("User deleted: {}", target_user.username);
Ok(Json(LoginResponse {
success: true,
message: Some("User deleted successfully".to_string()),
}))
}
/// Change password request
#[derive(Deserialize)]
pub struct ChangePasswordRequest {
@@ -2862,54 +2711,39 @@ pub struct ChangePasswordRequest {
pub new_password: String,
}
/// Change user password
pub async fn change_user_password(
/// Change current user's password
pub async fn change_password(
State(state): State<Arc<AppState>>,
Extension(session): Extension<Session>,
Path(user_id): Path<String>,
axum::Extension(session): axum::Extension<Session>,
Json(req): Json<ChangePasswordRequest>,
) -> Result<Json<LoginResponse>> {
// Check if current user is admin or changing own password
let current_user = state
.users
.get(&session.user_id)
.await?
.ok_or_else(|| AppError::AuthError("User not found".to_string()))?;
let is_self = user_id == session.user_id;
let is_admin = current_user.is_admin;
if !is_self && !is_admin {
return Err(AppError::Forbidden(
"Cannot change other user's password".to_string(),
));
}
// Validate new password
if req.new_password.len() < 4 {
return Err(AppError::BadRequest(
"Password must be at least 4 characters".to_string(),
));
}
// If changing own password, verify current password
if is_self {
let verified = state
.users
.verify(&current_user.username, &req.current_password)
.await?;
if verified.is_none() {
return Err(AppError::AuthError(
"Current password is incorrect".to_string(),
));
}
let verified = state
.users
.verify(&current_user.username, &req.current_password)
.await?;
if verified.is_none() {
return Err(AppError::AuthError(
"Current password is incorrect".to_string(),
));
}
state
.users
.update_password(&user_id, &req.new_password)
.update_password(&session.user_id, &req.new_password)
.await?;
info!("Password changed for user ID: {}", user_id);
info!("Password changed for user ID: {}", session.user_id);
Ok(Json(LoginResponse {
success: true,
@@ -2917,6 +2751,55 @@ pub async fn change_user_password(
}))
}
/// Change username request
#[derive(Deserialize)]
pub struct ChangeUsernameRequest {
pub username: String,
pub current_password: String,
}
/// Change current user's username
pub async fn change_username(
State(state): State<Arc<AppState>>,
axum::Extension(session): axum::Extension<Session>,
Json(req): Json<ChangeUsernameRequest>,
) -> Result<Json<LoginResponse>> {
let current_user = state
.users
.get(&session.user_id)
.await?
.ok_or_else(|| AppError::AuthError("User not found".to_string()))?;
if req.username.len() < 2 {
return Err(AppError::BadRequest(
"Username must be at least 2 characters".to_string(),
));
}
let verified = state
.users
.verify(&current_user.username, &req.current_password)
.await?;
if verified.is_none() {
return Err(AppError::AuthError(
"Current password is incorrect".to_string(),
));
}
if current_user.username != req.username {
state
.users
.update_username(&session.user_id, &req.username)
.await?;
}
info!("Username changed for user ID: {}", session.user_id);
Ok(Json(LoginResponse {
success: true,
message: Some("Username changed successfully".to_string()),
}))
}
// ============================================================================
// System Control
// ============================================================================