mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 03:32:00 +08:00
init
This commit is contained in:
362
src/web/handlers/config/apply.rs
Normal file
362
src/web/handlers/config/apply.rs
Normal file
@@ -0,0 +1,362 @@
|
||||
//! 配置热重载逻辑
|
||||
//!
|
||||
//! 从 handlers.rs 中抽取的配置应用函数,负责将配置变更应用到各个子系统。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::*;
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::state::AppState;
|
||||
|
||||
/// 应用 Video 配置变更
|
||||
pub async fn apply_video_config(
|
||||
state: &Arc<AppState>,
|
||||
old_config: &VideoConfig,
|
||||
new_config: &VideoConfig,
|
||||
) -> Result<()> {
|
||||
// 检查配置是否实际变更
|
||||
if old_config == new_config {
|
||||
tracing::info!("Video config unchanged, skipping reload");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::info!("Applying video config changes...");
|
||||
|
||||
let device = new_config
|
||||
.device
|
||||
.clone()
|
||||
.ok_or_else(|| AppError::BadRequest("video_device is required".to_string()))?;
|
||||
|
||||
let format = new_config
|
||||
.format
|
||||
.as_ref()
|
||||
.and_then(|f| {
|
||||
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);
|
||||
|
||||
// Step 1: 更新 WebRTC streamer 配置(停止现有 pipeline 和 sessions)
|
||||
state
|
||||
.stream_manager
|
||||
.webrtc_streamer()
|
||||
.update_video_config(resolution, format, new_config.fps)
|
||||
.await;
|
||||
tracing::info!("WebRTC streamer config updated");
|
||||
|
||||
// Step 2: 应用视频配置到 streamer(重新创建 capturer)
|
||||
state
|
||||
.stream_manager
|
||||
.streamer()
|
||||
.apply_video_config(&device, format, resolution, new_config.fps)
|
||||
.await
|
||||
.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 4: 更新 WebRTC frame source
|
||||
if let Some(frame_tx) = state.stream_manager.frame_sender().await {
|
||||
let receiver_count = frame_tx.receiver_count();
|
||||
state
|
||||
.stream_manager
|
||||
.webrtc_streamer()
|
||||
.set_video_source(frame_tx)
|
||||
.await;
|
||||
tracing::info!(
|
||||
"WebRTC streamer frame source updated (receiver_count={})",
|
||||
receiver_count
|
||||
);
|
||||
} else {
|
||||
tracing::warn!("No frame source available after config change");
|
||||
}
|
||||
|
||||
tracing::info!("Video config applied successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 应用 Stream 配置变更
|
||||
pub async fn apply_stream_config(
|
||||
state: &Arc<AppState>,
|
||||
old_config: &StreamConfig,
|
||||
new_config: &StreamConfig,
|
||||
) -> Result<()> {
|
||||
tracing::info!("Applying stream config changes...");
|
||||
|
||||
// 更新编码器后端
|
||||
if old_config.encoder != new_config.encoder {
|
||||
let encoder_backend = new_config.encoder.to_backend();
|
||||
tracing::info!(
|
||||
"Updating encoder backend to: {:?} (from config: {:?})",
|
||||
encoder_backend,
|
||||
new_config.encoder
|
||||
);
|
||||
state
|
||||
.stream_manager
|
||||
.webrtc_streamer()
|
||||
.update_encoder_backend(encoder_backend)
|
||||
.await;
|
||||
}
|
||||
|
||||
// 更新码率
|
||||
if old_config.bitrate_kbps != new_config.bitrate_kbps {
|
||||
state
|
||||
.stream_manager
|
||||
.webrtc_streamer()
|
||||
.set_bitrate(new_config.bitrate_kbps)
|
||||
.await
|
||||
.ok(); // Ignore error if no active stream
|
||||
}
|
||||
|
||||
// 更新 ICE 配置 (STUN/TURN)
|
||||
let ice_changed = old_config.stun_server != new_config.stun_server
|
||||
|| old_config.turn_server != new_config.turn_server
|
||||
|| old_config.turn_username != new_config.turn_username
|
||||
|| old_config.turn_password != new_config.turn_password;
|
||||
|
||||
if ice_changed {
|
||||
tracing::info!(
|
||||
"Updating ICE config: STUN={:?}, TURN={:?}",
|
||||
new_config.stun_server,
|
||||
new_config.turn_server
|
||||
);
|
||||
state
|
||||
.stream_manager
|
||||
.webrtc_streamer()
|
||||
.update_ice_config(
|
||||
new_config.stun_server.clone(),
|
||||
new_config.turn_server.clone(),
|
||||
new_config.turn_username.clone(),
|
||||
new_config.turn_password.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Stream config applied: encoder={:?}, bitrate={} kbps",
|
||||
new_config.encoder,
|
||||
new_config.bitrate_kbps
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 应用 HID 配置变更
|
||||
pub async fn apply_hid_config(
|
||||
state: &Arc<AppState>,
|
||||
old_config: &HidConfig,
|
||||
new_config: &HidConfig,
|
||||
) -> Result<()> {
|
||||
// 检查是否需要重载
|
||||
if old_config.backend == new_config.backend
|
||||
&& old_config.ch9329_port == new_config.ch9329_port
|
||||
&& old_config.ch9329_baudrate == new_config.ch9329_baudrate
|
||||
&& old_config.otg_udc == new_config.otg_udc
|
||||
{
|
||||
tracing::info!("HID config unchanged, skipping reload");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::info!("Applying HID config changes...");
|
||||
|
||||
let new_hid_backend = match new_config.backend {
|
||||
HidBackend::Otg => crate::hid::HidBackendType::Otg,
|
||||
HidBackend::Ch9329 => crate::hid::HidBackendType::Ch9329 {
|
||||
port: new_config.ch9329_port.clone(),
|
||||
baud_rate: new_config.ch9329_baudrate,
|
||||
},
|
||||
HidBackend::None => crate::hid::HidBackendType::None,
|
||||
};
|
||||
|
||||
state
|
||||
.hid
|
||||
.reload(new_hid_backend)
|
||||
.await
|
||||
.map_err(|e| AppError::Config(format!("HID reload failed: {}", e)))?;
|
||||
|
||||
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
|
||||
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.images_path,
|
||||
&config.msd.drive_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(())
|
||||
}
|
||||
|
||||
/// 应用 MSD 配置变更
|
||||
pub async fn apply_msd_config(
|
||||
state: &Arc<AppState>,
|
||||
old_config: &MsdConfig,
|
||||
new_config: &MsdConfig,
|
||||
) -> Result<()> {
|
||||
tracing::info!("MSD config sent, checking if reload needed...");
|
||||
tracing::debug!("Old MSD config: {:?}", old_config);
|
||||
tracing::debug!("New MSD config: {:?}", new_config);
|
||||
|
||||
// Check if MSD enabled state changed
|
||||
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);
|
||||
|
||||
if old_msd_enabled != new_msd_enabled {
|
||||
if new_msd_enabled {
|
||||
// MSD was disabled, now enabled - need to initialize
|
||||
tracing::info!("MSD enabled in config, initializing...");
|
||||
|
||||
let msd = crate::msd::MsdController::new(
|
||||
state.otg_service.clone(),
|
||||
&new_config.images_path,
|
||||
&new_config.drive_path,
|
||||
);
|
||||
msd.init().await.map_err(|e| {
|
||||
AppError::Config(format!("MSD initialization failed: {}", e))
|
||||
})?;
|
||||
|
||||
// Set event bus
|
||||
let events = state.events.clone();
|
||||
msd.set_event_bus(events).await;
|
||||
|
||||
// Store the initialized controller
|
||||
*state.msd.write().await = Some(msd);
|
||||
tracing::info!("MSD initialized successfully");
|
||||
} else {
|
||||
// MSD was enabled, now disabled - shutdown
|
||||
tracing::info!("MSD disabled in config, shutting down...");
|
||||
|
||||
if let Some(msd) = state.msd.write().await.as_mut() {
|
||||
if let Err(e) = msd.shutdown().await {
|
||||
tracing::warn!("MSD shutdown failed: {}", e);
|
||||
}
|
||||
}
|
||||
*state.msd.write().await = None;
|
||||
tracing::info!("MSD shutdown complete");
|
||||
}
|
||||
} else {
|
||||
tracing::info!(
|
||||
"MSD enabled state unchanged ({}), no reload needed",
|
||||
new_msd_enabled
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 应用 ATX 配置变更
|
||||
pub async fn apply_atx_config(
|
||||
state: &Arc<AppState>,
|
||||
_old_config: &AtxConfig,
|
||||
new_config: &AtxConfig,
|
||||
) -> Result<()> {
|
||||
tracing::info!("Applying ATX config changes...");
|
||||
|
||||
// Convert AtxConfig to AtxControllerConfig
|
||||
let controller_config = new_config.to_controller_config();
|
||||
|
||||
// Reload the ATX controller with new configuration
|
||||
let atx_guard = state.atx.read().await;
|
||||
if let Some(atx) = atx_guard.as_ref() {
|
||||
if let Err(e) = atx.reload(controller_config).await {
|
||||
tracing::error!("ATX reload failed: {}", e);
|
||||
return Err(AppError::Config(format!("ATX reload failed: {}", e)));
|
||||
}
|
||||
tracing::info!("ATX controller reloaded successfully");
|
||||
} else {
|
||||
// ATX controller not initialized, create a new one if enabled
|
||||
drop(atx_guard);
|
||||
|
||||
if new_config.enabled {
|
||||
tracing::info!("ATX enabled in config, initializing...");
|
||||
|
||||
let atx = crate::atx::AtxController::new(controller_config);
|
||||
if let Err(e) = atx.init().await {
|
||||
tracing::warn!("ATX initialization failed: {}", e);
|
||||
} else {
|
||||
*state.atx.write().await = Some(atx);
|
||||
tracing::info!("ATX controller initialized successfully");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 应用 Audio 配置变更
|
||||
pub async fn apply_audio_config(
|
||||
state: &Arc<AppState>,
|
||||
_old_config: &AudioConfig,
|
||||
new_config: &AudioConfig,
|
||||
) -> Result<()> {
|
||||
tracing::info!("Applying audio config changes...");
|
||||
|
||||
// Create audio controller config from new config
|
||||
let audio_config = crate::audio::AudioControllerConfig {
|
||||
enabled: new_config.enabled,
|
||||
device: new_config.device.clone(),
|
||||
quality: crate::audio::AudioQuality::from_str(&new_config.quality),
|
||||
};
|
||||
|
||||
// Update audio controller
|
||||
if let Err(e) = state.audio.update_config(audio_config).await {
|
||||
tracing::error!("Audio config update failed: {}", e);
|
||||
// Don't fail - audio errors are not critical
|
||||
} else {
|
||||
tracing::info!(
|
||||
"Audio config applied: enabled={}, device={}",
|
||||
new_config.enabled,
|
||||
new_config.device
|
||||
);
|
||||
}
|
||||
|
||||
// Also update WebRTC audio enabled state
|
||||
if let Err(e) = state
|
||||
.stream_manager
|
||||
.set_webrtc_audio_enabled(new_config.enabled)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to update WebRTC audio state: {}", e);
|
||||
} else {
|
||||
tracing::info!("WebRTC audio enabled: {}", new_config.enabled);
|
||||
}
|
||||
|
||||
// Reconnect audio sources for existing WebRTC sessions
|
||||
if new_config.enabled {
|
||||
state.stream_manager.reconnect_webrtc_audio_sources().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
46
src/web/handlers/config/atx.rs
Normal file
46
src/web/handlers/config/atx.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
//! ATX 配置 Handler
|
||||
|
||||
use axum::{extract::State, Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::AtxConfig;
|
||||
use crate::error::Result;
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::apply::apply_atx_config;
|
||||
use super::types::AtxConfigUpdate;
|
||||
|
||||
/// 获取 ATX 配置
|
||||
pub async fn get_atx_config(State(state): State<Arc<AppState>>) -> Json<AtxConfig> {
|
||||
Json(state.config.get().atx.clone())
|
||||
}
|
||||
|
||||
/// 更新 ATX 配置
|
||||
pub async fn update_atx_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<AtxConfigUpdate>,
|
||||
) -> Result<Json<AtxConfig>> {
|
||||
// 1. 验证请求
|
||||
req.validate()?;
|
||||
|
||||
// 2. 获取旧配置
|
||||
let old_atx_config = state.config.get().atx.clone();
|
||||
|
||||
// 3. 应用更新到配置存储
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
req.apply_to(&mut config.atx);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// 4. 获取新配置
|
||||
let new_atx_config = state.config.get().atx.clone();
|
||||
|
||||
// 5. 应用到子系统(热重载)
|
||||
if let Err(e) = apply_atx_config(&state, &old_atx_config, &new_atx_config).await {
|
||||
tracing::error!("Failed to apply ATX config: {}", e);
|
||||
}
|
||||
|
||||
Ok(Json(new_atx_config))
|
||||
}
|
||||
46
src/web/handlers/config/audio.rs
Normal file
46
src/web/handlers/config/audio.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
//! Audio 配置 Handler
|
||||
|
||||
use axum::{extract::State, Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::AudioConfig;
|
||||
use crate::error::Result;
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::apply::apply_audio_config;
|
||||
use super::types::AudioConfigUpdate;
|
||||
|
||||
/// 获取 Audio 配置
|
||||
pub async fn get_audio_config(State(state): State<Arc<AppState>>) -> Json<AudioConfig> {
|
||||
Json(state.config.get().audio.clone())
|
||||
}
|
||||
|
||||
/// 更新 Audio 配置
|
||||
pub async fn update_audio_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<AudioConfigUpdate>,
|
||||
) -> Result<Json<AudioConfig>> {
|
||||
// 1. 验证请求
|
||||
req.validate()?;
|
||||
|
||||
// 2. 获取旧配置
|
||||
let old_audio_config = state.config.get().audio.clone();
|
||||
|
||||
// 3. 应用更新到配置存储
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
req.apply_to(&mut config.audio);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// 4. 获取新配置
|
||||
let new_audio_config = state.config.get().audio.clone();
|
||||
|
||||
// 5. 应用到子系统(热重载)
|
||||
if let Err(e) = apply_audio_config(&state, &old_audio_config, &new_audio_config).await {
|
||||
tracing::error!("Failed to apply audio config: {}", e);
|
||||
}
|
||||
|
||||
Ok(Json(new_audio_config))
|
||||
}
|
||||
46
src/web/handlers/config/hid.rs
Normal file
46
src/web/handlers/config/hid.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
//! HID 配置 Handler
|
||||
|
||||
use axum::{extract::State, Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::HidConfig;
|
||||
use crate::error::Result;
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::apply::apply_hid_config;
|
||||
use super::types::HidConfigUpdate;
|
||||
|
||||
/// 获取 HID 配置
|
||||
pub async fn get_hid_config(State(state): State<Arc<AppState>>) -> Json<HidConfig> {
|
||||
Json(state.config.get().hid.clone())
|
||||
}
|
||||
|
||||
/// 更新 HID 配置
|
||||
pub async fn update_hid_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<HidConfigUpdate>,
|
||||
) -> Result<Json<HidConfig>> {
|
||||
// 1. 验证请求
|
||||
req.validate()?;
|
||||
|
||||
// 2. 获取旧配置
|
||||
let old_hid_config = state.config.get().hid.clone();
|
||||
|
||||
// 3. 应用更新到配置存储
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
req.apply_to(&mut config.hid);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// 4. 获取新配置
|
||||
let new_hid_config = state.config.get().hid.clone();
|
||||
|
||||
// 5. 应用到子系统(热重载)
|
||||
if let Err(e) = apply_hid_config(&state, &old_hid_config, &new_hid_config).await {
|
||||
tracing::error!("Failed to apply HID config: {}", e);
|
||||
}
|
||||
|
||||
Ok(Json(new_hid_config))
|
||||
}
|
||||
48
src/web/handlers/config/mod.rs
Normal file
48
src/web/handlers/config/mod.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
//! 配置管理 Handler 模块
|
||||
//!
|
||||
//! 提供 RESTful 域分离的配置 API:
|
||||
//! - GET /api/config/video - 获取视频配置
|
||||
//! - PATCH /api/config/video - 更新视频配置
|
||||
//! - GET /api/config/stream - 获取流配置
|
||||
//! - PATCH /api/config/stream - 更新流配置
|
||||
//! - GET /api/config/hid - 获取 HID 配置
|
||||
//! - PATCH /api/config/hid - 更新 HID 配置
|
||||
//! - GET /api/config/msd - 获取 MSD 配置
|
||||
//! - PATCH /api/config/msd - 更新 MSD 配置
|
||||
//! - GET /api/config/atx - 获取 ATX 配置
|
||||
//! - PATCH /api/config/atx - 更新 ATX 配置
|
||||
//! - GET /api/config/audio - 获取音频配置
|
||||
//! - PATCH /api/config/audio - 更新音频配置
|
||||
|
||||
mod apply;
|
||||
mod types;
|
||||
|
||||
mod video;
|
||||
mod stream;
|
||||
mod hid;
|
||||
mod msd;
|
||||
mod atx;
|
||||
mod audio;
|
||||
|
||||
// 导出 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};
|
||||
|
||||
// 保留全局配置查询(向后兼容)
|
||||
use axum::{extract::State, Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::AppConfig;
|
||||
use crate::state::AppState;
|
||||
|
||||
/// 获取完整配置
|
||||
pub async fn get_all_config(State(state): State<Arc<AppState>>) -> Json<AppConfig> {
|
||||
let mut config = (*state.config.get()).clone();
|
||||
// 不暴露敏感信息
|
||||
config.auth.totp_secret = None;
|
||||
Json(config)
|
||||
}
|
||||
46
src/web/handlers/config/msd.rs
Normal file
46
src/web/handlers/config/msd.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
//! MSD 配置 Handler
|
||||
|
||||
use axum::{extract::State, Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::MsdConfig;
|
||||
use crate::error::Result;
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::apply::apply_msd_config;
|
||||
use super::types::MsdConfigUpdate;
|
||||
|
||||
/// 获取 MSD 配置
|
||||
pub async fn get_msd_config(State(state): State<Arc<AppState>>) -> Json<MsdConfig> {
|
||||
Json(state.config.get().msd.clone())
|
||||
}
|
||||
|
||||
/// 更新 MSD 配置
|
||||
pub async fn update_msd_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<MsdConfigUpdate>,
|
||||
) -> Result<Json<MsdConfig>> {
|
||||
// 1. 验证请求
|
||||
req.validate()?;
|
||||
|
||||
// 2. 获取旧配置
|
||||
let old_msd_config = state.config.get().msd.clone();
|
||||
|
||||
// 3. 应用更新到配置存储
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
req.apply_to(&mut config.msd);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// 4. 获取新配置
|
||||
let new_msd_config = state.config.get().msd.clone();
|
||||
|
||||
// 5. 应用到子系统(热重载)
|
||||
if let Err(e) = apply_msd_config(&state, &old_msd_config, &new_msd_config).await {
|
||||
tracing::error!("Failed to apply MSD config: {}", e);
|
||||
}
|
||||
|
||||
Ok(Json(new_msd_config))
|
||||
}
|
||||
46
src/web/handlers/config/stream.rs
Normal file
46
src/web/handlers/config/stream.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
//! Stream 配置 Handler
|
||||
|
||||
use axum::{extract::State, Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::apply::apply_stream_config;
|
||||
use super::types::{StreamConfigResponse, StreamConfigUpdate};
|
||||
|
||||
/// 获取 Stream 配置
|
||||
pub async fn get_stream_config(State(state): State<Arc<AppState>>) -> Json<StreamConfigResponse> {
|
||||
let config = state.config.get();
|
||||
Json(StreamConfigResponse::from(&config.stream))
|
||||
}
|
||||
|
||||
/// 更新 Stream 配置
|
||||
pub async fn update_stream_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<StreamConfigUpdate>,
|
||||
) -> Result<Json<StreamConfigResponse>> {
|
||||
// 1. 验证请求
|
||||
req.validate()?;
|
||||
|
||||
// 2. 获取旧配置
|
||||
let old_stream_config = state.config.get().stream.clone();
|
||||
|
||||
// 3. 应用更新到配置存储
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
req.apply_to(&mut config.stream);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// 4. 获取新配置
|
||||
let new_stream_config = state.config.get().stream.clone();
|
||||
|
||||
// 5. 应用到子系统(热重载)
|
||||
if let Err(e) = apply_stream_config(&state, &old_stream_config, &new_stream_config).await {
|
||||
tracing::error!("Failed to apply stream config: {}", e);
|
||||
}
|
||||
|
||||
Ok(Json(StreamConfigResponse::from(&new_stream_config)))
|
||||
}
|
||||
396
src/web/handlers/config/types.rs
Normal file
396
src/web/handlers/config/types.rs
Normal file
@@ -0,0 +1,396 @@
|
||||
use serde::Deserialize;
|
||||
use typeshare::typeshare;
|
||||
use crate::config::*;
|
||||
use crate::error::AppError;
|
||||
|
||||
// ===== Video Config =====
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct VideoConfigUpdate {
|
||||
pub device: Option<String>,
|
||||
pub format: Option<String>,
|
||||
pub width: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
pub fps: Option<u32>,
|
||||
pub quality: Option<u32>,
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
if let Some(height) = self.height {
|
||||
if !(240..=4320).contains(&height) {
|
||||
return Err(AppError::BadRequest("Invalid height: must be 240-4320".into()));
|
||||
}
|
||||
}
|
||||
if let Some(fps) = self.fps {
|
||||
if !(1..=120).contains(&fps) {
|
||||
return Err(AppError::BadRequest("Invalid fps: must be 1-120".into()));
|
||||
}
|
||||
}
|
||||
if let Some(quality) = self.quality {
|
||||
if !(1..=100).contains(&quality) {
|
||||
return Err(AppError::BadRequest("Invalid quality: must be 1-100".into()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, config: &mut VideoConfig) {
|
||||
if let Some(ref device) = self.device {
|
||||
config.device = Some(device.clone());
|
||||
}
|
||||
if let Some(ref format) = self.format {
|
||||
config.format = Some(format.clone());
|
||||
}
|
||||
if let Some(width) = self.width {
|
||||
config.width = width;
|
||||
}
|
||||
if let Some(height) = self.height {
|
||||
config.height = height;
|
||||
}
|
||||
if let Some(fps) = self.fps {
|
||||
config.fps = fps;
|
||||
}
|
||||
if let Some(quality) = self.quality {
|
||||
config.quality = quality;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Stream Config =====
|
||||
|
||||
/// Stream 配置响应(包含 has_turn_password 字段)
|
||||
#[typeshare]
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct StreamConfigResponse {
|
||||
pub mode: StreamMode,
|
||||
pub encoder: EncoderType,
|
||||
pub bitrate_kbps: u32,
|
||||
pub gop_size: u32,
|
||||
pub stun_server: Option<String>,
|
||||
pub turn_server: Option<String>,
|
||||
pub turn_username: Option<String>,
|
||||
/// 指示是否已设置 TURN 密码(实际密码不返回)
|
||||
pub has_turn_password: bool,
|
||||
}
|
||||
|
||||
impl From<&StreamConfig> for StreamConfigResponse {
|
||||
fn from(config: &StreamConfig) -> Self {
|
||||
Self {
|
||||
mode: config.mode.clone(),
|
||||
encoder: config.encoder.clone(),
|
||||
bitrate_kbps: config.bitrate_kbps,
|
||||
gop_size: config.gop_size,
|
||||
stun_server: config.stun_server.clone(),
|
||||
turn_server: config.turn_server.clone(),
|
||||
turn_username: config.turn_username.clone(),
|
||||
has_turn_password: config.turn_password.is_some(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StreamConfigUpdate {
|
||||
pub mode: Option<StreamMode>,
|
||||
pub encoder: Option<EncoderType>,
|
||||
pub bitrate_kbps: Option<u32>,
|
||||
pub gop_size: Option<u32>,
|
||||
/// STUN server URL (e.g., "stun:stun.l.google.com:19302")
|
||||
pub stun_server: Option<String>,
|
||||
/// TURN server URL (e.g., "turn:turn.example.com:3478")
|
||||
pub turn_server: Option<String>,
|
||||
/// TURN username
|
||||
pub turn_username: Option<String>,
|
||||
/// TURN password
|
||||
pub turn_password: Option<String>,
|
||||
}
|
||||
|
||||
impl StreamConfigUpdate {
|
||||
pub fn validate(&self) -> crate::error::Result<()> {
|
||||
if let Some(bitrate) = self.bitrate_kbps {
|
||||
if !(1000..=15000).contains(&bitrate) {
|
||||
return Err(AppError::BadRequest("Bitrate must be 1000-15000 kbps".into()));
|
||||
}
|
||||
}
|
||||
if let Some(gop) = self.gop_size {
|
||||
if !(10..=300).contains(&gop) {
|
||||
return Err(AppError::BadRequest("GOP size must be 10-300".into()));
|
||||
}
|
||||
}
|
||||
// Validate STUN server format
|
||||
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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
// Validate TURN server format
|
||||
if let Some(ref turn) = self.turn_server {
|
||||
if !turn.is_empty() && !turn.starts_with("turn:") && !turn.starts_with("turns:") {
|
||||
return Err(AppError::BadRequest(
|
||||
"TURN server must start with 'turn:' or 'turns:' (e.g., turn:turn.example.com:3478)".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, config: &mut StreamConfig) {
|
||||
if let Some(mode) = self.mode.clone() {
|
||||
config.mode = mode;
|
||||
}
|
||||
if let Some(encoder) = self.encoder.clone() {
|
||||
config.encoder = encoder;
|
||||
}
|
||||
if let Some(bitrate) = self.bitrate_kbps {
|
||||
config.bitrate_kbps = bitrate;
|
||||
}
|
||||
if let Some(gop) = self.gop_size {
|
||||
config.gop_size = gop;
|
||||
}
|
||||
// STUN/TURN settings - empty string means clear, Some("value") means set
|
||||
if let Some(ref stun) = self.stun_server {
|
||||
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()) };
|
||||
}
|
||||
if let Some(ref username) = self.turn_username {
|
||||
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()) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== HID Config =====
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct HidConfigUpdate {
|
||||
pub backend: Option<HidBackend>,
|
||||
pub ch9329_port: Option<String>,
|
||||
pub ch9329_baudrate: Option<u32>,
|
||||
pub otg_udc: Option<String>,
|
||||
pub mouse_absolute: Option<bool>,
|
||||
}
|
||||
|
||||
impl HidConfigUpdate {
|
||||
pub fn validate(&self) -> crate::error::Result<()> {
|
||||
if let Some(baudrate) = self.ch9329_baudrate {
|
||||
let valid_rates = [9600, 19200, 38400, 57600, 115200];
|
||||
if !valid_rates.contains(&baudrate) {
|
||||
return Err(AppError::BadRequest(
|
||||
"Invalid baudrate: must be 9600, 19200, 38400, 57600, or 115200".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, config: &mut HidConfig) {
|
||||
if let Some(backend) = self.backend.clone() {
|
||||
config.backend = backend;
|
||||
}
|
||||
if let Some(ref port) = self.ch9329_port {
|
||||
config.ch9329_port = port.clone();
|
||||
}
|
||||
if let Some(baudrate) = self.ch9329_baudrate {
|
||||
config.ch9329_baudrate = baudrate;
|
||||
}
|
||||
if let Some(ref udc) = self.otg_udc {
|
||||
config.otg_udc = Some(udc.clone());
|
||||
}
|
||||
if let Some(absolute) = self.mouse_absolute {
|
||||
config.mouse_absolute = absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== MSD Config =====
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MsdConfigUpdate {
|
||||
pub enabled: Option<bool>,
|
||||
pub images_path: Option<String>,
|
||||
pub drive_path: Option<String>,
|
||||
pub virtual_drive_size_mb: Option<u32>,
|
||||
}
|
||||
|
||||
impl MsdConfigUpdate {
|
||||
pub fn validate(&self) -> crate::error::Result<()> {
|
||||
if let Some(size) = self.virtual_drive_size_mb {
|
||||
if !(1..=10240).contains(&size) {
|
||||
return Err(AppError::BadRequest("Drive size must be 1-10240 MB".into()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, config: &mut MsdConfig) {
|
||||
if let Some(enabled) = self.enabled {
|
||||
config.enabled = enabled;
|
||||
}
|
||||
if let Some(ref path) = self.images_path {
|
||||
config.images_path = path.clone();
|
||||
}
|
||||
if let Some(ref path) = self.drive_path {
|
||||
config.drive_path = path.clone();
|
||||
}
|
||||
if let Some(size) = self.virtual_drive_size_mb {
|
||||
config.virtual_drive_size_mb = size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== ATX Config =====
|
||||
|
||||
/// Update for a single ATX key configuration
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AtxKeyConfigUpdate {
|
||||
pub driver: Option<crate::atx::AtxDriverType>,
|
||||
pub device: Option<String>,
|
||||
pub pin: Option<u32>,
|
||||
pub active_level: Option<crate::atx::ActiveLevel>,
|
||||
}
|
||||
|
||||
/// Update for LED sensing configuration
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AtxLedConfigUpdate {
|
||||
pub enabled: Option<bool>,
|
||||
pub gpio_chip: Option<String>,
|
||||
pub gpio_pin: Option<u32>,
|
||||
pub inverted: Option<bool>,
|
||||
}
|
||||
|
||||
/// ATX configuration update request
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AtxConfigUpdate {
|
||||
pub enabled: Option<bool>,
|
||||
/// Power button configuration
|
||||
pub power: Option<AtxKeyConfigUpdate>,
|
||||
/// Reset button configuration
|
||||
pub reset: Option<AtxKeyConfigUpdate>,
|
||||
/// LED sensing configuration
|
||||
pub led: Option<AtxLedConfigUpdate>,
|
||||
/// Network interface for WOL packets (empty = auto)
|
||||
pub wol_interface: Option<String>,
|
||||
}
|
||||
|
||||
impl AtxConfigUpdate {
|
||||
pub fn validate(&self) -> crate::error::Result<()> {
|
||||
// Validate power key config if present
|
||||
if let Some(ref power) = self.power {
|
||||
Self::validate_key_config(power, "power")?;
|
||||
}
|
||||
// Validate reset key config if present
|
||||
if let Some(ref reset) = self.reset {
|
||||
Self::validate_key_config(reset, "reset")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_key_config(key: &AtxKeyConfigUpdate, name: &str) -> crate::error::Result<()> {
|
||||
if let Some(ref device) = key.device {
|
||||
if !device.is_empty() && !std::path::Path::new(device).exists() {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"{} device '{}' does not exist",
|
||||
name, device
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, config: &mut AtxConfig) {
|
||||
if let Some(enabled) = self.enabled {
|
||||
config.enabled = enabled;
|
||||
}
|
||||
if let Some(ref power) = self.power {
|
||||
Self::apply_key_update(power, &mut config.power);
|
||||
}
|
||||
if let Some(ref reset) = self.reset {
|
||||
Self::apply_key_update(reset, &mut config.reset);
|
||||
}
|
||||
if let Some(ref led) = self.led {
|
||||
Self::apply_led_update(led, &mut config.led);
|
||||
}
|
||||
if let Some(ref wol_interface) = self.wol_interface {
|
||||
config.wol_interface = wol_interface.clone();
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_key_update(update: &AtxKeyConfigUpdate, config: &mut crate::atx::AtxKeyConfig) {
|
||||
if let Some(driver) = update.driver {
|
||||
config.driver = driver;
|
||||
}
|
||||
if let Some(ref device) = update.device {
|
||||
config.device = device.clone();
|
||||
}
|
||||
if let Some(pin) = update.pin {
|
||||
config.pin = pin;
|
||||
}
|
||||
if let Some(level) = update.active_level {
|
||||
config.active_level = level;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_led_update(update: &AtxLedConfigUpdate, config: &mut crate::atx::AtxLedConfig) {
|
||||
if let Some(enabled) = update.enabled {
|
||||
config.enabled = enabled;
|
||||
}
|
||||
if let Some(ref chip) = update.gpio_chip {
|
||||
config.gpio_chip = chip.clone();
|
||||
}
|
||||
if let Some(pin) = update.gpio_pin {
|
||||
config.gpio_pin = pin;
|
||||
}
|
||||
if let Some(inverted) = update.inverted {
|
||||
config.inverted = inverted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Audio Config =====
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AudioConfigUpdate {
|
||||
pub enabled: Option<bool>,
|
||||
pub device: Option<String>,
|
||||
pub quality: Option<String>,
|
||||
}
|
||||
|
||||
impl AudioConfigUpdate {
|
||||
pub fn validate(&self) -> crate::error::Result<()> {
|
||||
if let Some(ref quality) = self.quality {
|
||||
if !["voice", "balanced", "high"].contains(&quality.as_str()) {
|
||||
return Err(AppError::BadRequest(
|
||||
"Invalid quality: must be 'voice', 'balanced', or 'high'".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, config: &mut AudioConfig) {
|
||||
if let Some(enabled) = self.enabled {
|
||||
config.enabled = enabled;
|
||||
}
|
||||
if let Some(ref device) = self.device {
|
||||
config.device = device.clone();
|
||||
}
|
||||
if let Some(ref quality) = self.quality {
|
||||
config.quality = quality.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/web/handlers/config/video.rs
Normal file
47
src/web/handlers/config/video.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! Video 配置 Handler
|
||||
|
||||
use axum::{extract::State, Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::VideoConfig;
|
||||
use crate::error::Result;
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::apply::apply_video_config;
|
||||
use super::types::VideoConfigUpdate;
|
||||
|
||||
/// 获取 Video 配置
|
||||
pub async fn get_video_config(State(state): State<Arc<AppState>>) -> Json<VideoConfig> {
|
||||
Json(state.config.get().video.clone())
|
||||
}
|
||||
|
||||
/// 更新 Video 配置
|
||||
pub async fn update_video_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<VideoConfigUpdate>,
|
||||
) -> Result<Json<VideoConfig>> {
|
||||
// 1. 验证请求
|
||||
req.validate()?;
|
||||
|
||||
// 2. 获取旧配置
|
||||
let old_video_config = state.config.get().video.clone();
|
||||
|
||||
// 3. 应用更新到配置存储
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
req.apply_to(&mut config.video);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// 4. 获取新配置
|
||||
let new_video_config = state.config.get().video.clone();
|
||||
|
||||
// 5. 应用到子系统(热重载)
|
||||
if let Err(e) = apply_video_config(&state, &old_video_config, &new_video_config).await {
|
||||
tracing::error!("Failed to apply video config: {}", e);
|
||||
// 根据用户选择,仅记录错误,不回滚
|
||||
}
|
||||
|
||||
Ok(Json(new_video_config))
|
||||
}
|
||||
Reference in New Issue
Block a user