mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-29 15:31:46 +08:00
feat: 新增 MJPEG/H.264 VNC 初步支持
This commit is contained in:
@@ -6,7 +6,8 @@ use crate::rtsp::RtspService;
|
||||
use crate::state::AppState;
|
||||
use crate::stream_encoder::encoder_type_to_backend;
|
||||
use crate::video::codec_constraints::{
|
||||
enforce_constraints_with_stream_manager, StreamCodecConstraints,
|
||||
enforce_constraints_with_stream_manager, validate_third_party_codec_compatibility,
|
||||
StreamCodecConstraints,
|
||||
};
|
||||
use tokio::sync::{Mutex, OwnedMutexGuard};
|
||||
|
||||
@@ -409,6 +410,27 @@ pub async fn enforce_stream_codec_constraints(state: &Arc<AppState>) -> Result<O
|
||||
Ok(enforcement.message)
|
||||
}
|
||||
|
||||
fn validate_rustdesk_candidate(
|
||||
state: &Arc<AppState>,
|
||||
new_config: &crate::rustdesk::config::RustDeskConfig,
|
||||
) -> Result<()> {
|
||||
let mut candidate = state.config.get().as_ref().clone();
|
||||
candidate.rustdesk = new_config.clone();
|
||||
validate_third_party_codec_compatibility(&candidate)
|
||||
}
|
||||
|
||||
fn validate_vnc_candidate(state: &Arc<AppState>, new_config: &VncConfig) -> Result<()> {
|
||||
let mut candidate = state.config.get().as_ref().clone();
|
||||
candidate.vnc = new_config.clone();
|
||||
validate_third_party_codec_compatibility(&candidate)
|
||||
}
|
||||
|
||||
fn validate_rtsp_candidate(state: &Arc<AppState>, new_config: &RtspConfig) -> Result<()> {
|
||||
let mut candidate = state.config.get().as_ref().clone();
|
||||
candidate.rtsp = new_config.clone();
|
||||
validate_third_party_codec_compatibility(&candidate)
|
||||
}
|
||||
|
||||
pub async fn apply_rustdesk_config(
|
||||
state: &Arc<AppState>,
|
||||
old_config: &crate::rustdesk::config::RustDeskConfig,
|
||||
@@ -417,6 +439,8 @@ pub async fn apply_rustdesk_config(
|
||||
) -> Result<()> {
|
||||
tracing::info!("Applying RustDesk config changes...");
|
||||
|
||||
validate_rustdesk_candidate(state, new_config)?;
|
||||
|
||||
let mut rustdesk_guard = state.rustdesk.write().await;
|
||||
let mut credentials_to_save = None;
|
||||
|
||||
@@ -433,6 +457,7 @@ pub async fn apply_rustdesk_config(
|
||||
|
||||
if new_config.enabled {
|
||||
let need_restart = options.force
|
||||
|| old_config.codec != new_config.codec
|
||||
|| old_config.rendezvous_server != new_config.rendezvous_server
|
||||
|| old_config.device_id != new_config.device_id
|
||||
|| old_config.device_password != new_config.device_password;
|
||||
@@ -488,6 +513,77 @@ pub async fn apply_rustdesk_config(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn apply_vnc_config(
|
||||
state: &Arc<AppState>,
|
||||
old_config: &VncConfig,
|
||||
new_config: &VncConfig,
|
||||
options: ConfigApplyOptions,
|
||||
) -> Result<()> {
|
||||
tracing::info!("Applying VNC config changes...");
|
||||
|
||||
validate_vnc_candidate(state, new_config)?;
|
||||
|
||||
if new_config.enabled {
|
||||
let mut candidate = state.config.get().as_ref().clone();
|
||||
candidate.vnc = new_config.clone();
|
||||
let constraints = StreamCodecConstraints::from_config(&candidate);
|
||||
match enforce_constraints_with_stream_manager(&state.stream_manager, &constraints).await {
|
||||
Ok(result) if result.changed => {
|
||||
if let Some(message) = result.message {
|
||||
tracing::info!("{}", message);
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => tracing::warn!(
|
||||
"Failed to enforce VNC stream constraints before start: {}",
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
let mut vnc_guard = state.vnc.write().await;
|
||||
|
||||
if !new_config.enabled {
|
||||
if let Some(ref service) = *vnc_guard {
|
||||
service.stop().await?;
|
||||
}
|
||||
*vnc_guard = None;
|
||||
}
|
||||
|
||||
if new_config.enabled {
|
||||
let need_restart = options.force
|
||||
|| old_config.bind != new_config.bind
|
||||
|| old_config.port != new_config.port
|
||||
|| old_config.encoding != new_config.encoding
|
||||
|| old_config.password != new_config.password
|
||||
|| old_config.jpeg_quality != new_config.jpeg_quality
|
||||
|| old_config.allow_one_client != new_config.allow_one_client;
|
||||
|
||||
if vnc_guard.is_none() {
|
||||
let service = crate::vnc::VncService::new(
|
||||
new_config.clone(),
|
||||
state.stream_manager.clone(),
|
||||
state.hid.clone(),
|
||||
);
|
||||
service.start().await?;
|
||||
*vnc_guard = Some(Arc::new(service));
|
||||
tracing::info!("VNC service started");
|
||||
} else if need_restart {
|
||||
if let Some(ref service) = *vnc_guard {
|
||||
service.restart(new_config.clone()).await?;
|
||||
tracing::info!("VNC service restarted");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(vnc_guard);
|
||||
if let Some(message) = enforce_stream_codec_constraints(state).await? {
|
||||
tracing::info!("{}", message);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn apply_rtsp_config(
|
||||
state: &Arc<AppState>,
|
||||
old_config: &RtspConfig,
|
||||
@@ -496,6 +592,8 @@ pub async fn apply_rtsp_config(
|
||||
) -> Result<()> {
|
||||
tracing::info!("Applying RTSP config changes...");
|
||||
|
||||
validate_rtsp_candidate(state, new_config)?;
|
||||
|
||||
let mut rtsp_guard = state.rtsp.write().await;
|
||||
|
||||
if !new_config.enabled {
|
||||
|
||||
@@ -12,6 +12,7 @@ mod rtsp;
|
||||
mod rustdesk;
|
||||
mod stream;
|
||||
pub(crate) mod video;
|
||||
mod vnc;
|
||||
mod web;
|
||||
|
||||
pub use atx::{get_atx_config, update_atx_config};
|
||||
@@ -31,6 +32,9 @@ pub use rustdesk::{
|
||||
};
|
||||
pub use stream::{get_stream_config, update_stream_config};
|
||||
pub use video::{get_video_config, update_video_config};
|
||||
pub use vnc::{
|
||||
get_vnc_config, get_vnc_status, start_vnc_service, stop_vnc_service, update_vnc_config,
|
||||
};
|
||||
pub use web::{get_web_config, update_web_config};
|
||||
|
||||
use axum::{extract::State, Json};
|
||||
@@ -52,6 +56,7 @@ fn sanitize_config_for_api(config: &mut AppConfig) {
|
||||
config.rustdesk.signing_private_key = None;
|
||||
|
||||
config.rtsp.password = None;
|
||||
config.vnc.password = None;
|
||||
}
|
||||
|
||||
pub async fn get_all_config(State(state): State<Arc<AppState>>) -> Json<AppConfig> {
|
||||
|
||||
@@ -7,6 +7,44 @@ use crate::state::AppState;
|
||||
use super::apply::{apply_rtsp_config, try_apply_lock, ConfigApplyOptions};
|
||||
use super::types::{RtspConfigResponse, RtspConfigUpdate, RtspStatusResponse};
|
||||
|
||||
fn validate_candidate(state: &Arc<AppState>, config: &crate::config::RtspConfig) -> Result<()> {
|
||||
let mut candidate = state.config.get().as_ref().clone();
|
||||
candidate.rtsp = config.clone();
|
||||
crate::video::codec_constraints::validate_third_party_codec_compatibility(&candidate)
|
||||
}
|
||||
|
||||
async fn persist_and_apply(
|
||||
state: &Arc<AppState>,
|
||||
old_config: crate::config::RtspConfig,
|
||||
new_config: crate::config::RtspConfig,
|
||||
) -> Result<crate::config::RtspConfig> {
|
||||
validate_candidate(state, &new_config)?;
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
config.rtsp = new_config.clone();
|
||||
})
|
||||
.await?;
|
||||
let stored_config = state.config.get().rtsp.clone();
|
||||
apply_rtsp_config(
|
||||
state,
|
||||
&old_config,
|
||||
&stored_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
Ok(stored_config)
|
||||
}
|
||||
|
||||
async fn current_status(state: &Arc<AppState>) -> crate::rtsp::RtspServiceStatus {
|
||||
let guard = state.rtsp.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
service.status().await
|
||||
} else {
|
||||
crate::rtsp::RtspServiceStatus::Stopped
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_rtsp_config(State(state): State<Arc<AppState>>) -> Json<RtspConfigResponse> {
|
||||
let config = state.config.get();
|
||||
Json(RtspConfigResponse::from(&config.rtsp))
|
||||
@@ -14,14 +52,7 @@ pub async fn get_rtsp_config(State(state): State<Arc<AppState>>) -> Json<RtspCon
|
||||
|
||||
pub async fn get_rtsp_status(State(state): State<Arc<AppState>>) -> Json<RtspStatusResponse> {
|
||||
let config = state.config.get().rtsp.clone();
|
||||
let status = {
|
||||
let guard = state.rtsp.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
service.status().await
|
||||
} else {
|
||||
crate::rtsp::RtspServiceStatus::Stopped
|
||||
}
|
||||
};
|
||||
let status = current_status(&state).await;
|
||||
|
||||
Json(RtspStatusResponse::new(&config, status))
|
||||
}
|
||||
@@ -34,22 +65,9 @@ pub async fn update_rtsp_config(
|
||||
|
||||
let _apply_guard = try_apply_lock(&state.config_apply_locks.rtsp, "rtsp")?;
|
||||
let old_config = state.config.get().rtsp.clone();
|
||||
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
req.apply_to(&mut config.rtsp);
|
||||
})
|
||||
.await?;
|
||||
|
||||
let new_config = state.config.get().rtsp.clone();
|
||||
apply_rtsp_config(
|
||||
&state,
|
||||
&old_config,
|
||||
&new_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
let mut merged_config = old_config.clone();
|
||||
req.apply_to(&mut merged_config);
|
||||
let new_config = persist_and_apply(&state, old_config, merged_config).await?;
|
||||
|
||||
Ok(Json(RtspConfigResponse::from(&new_config)))
|
||||
}
|
||||
@@ -61,25 +79,10 @@ pub async fn start_rtsp_service(
|
||||
let current_config = state.config.get().rtsp.clone();
|
||||
let mut start_config = current_config.clone();
|
||||
start_config.enabled = true;
|
||||
let stored_config = persist_and_apply(&state, current_config, start_config).await?;
|
||||
let status = current_status(&state).await;
|
||||
|
||||
apply_rtsp_config(
|
||||
&state,
|
||||
¤t_config,
|
||||
&start_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let status = {
|
||||
let guard = state.rtsp.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
service.status().await
|
||||
} else {
|
||||
crate::rtsp::RtspServiceStatus::Stopped
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(RtspStatusResponse::new(¤t_config, status)))
|
||||
Ok(Json(RtspStatusResponse::new(&stored_config, status)))
|
||||
}
|
||||
|
||||
pub async fn stop_rtsp_service(
|
||||
@@ -90,22 +93,8 @@ pub async fn stop_rtsp_service(
|
||||
let mut stop_config = current_config.clone();
|
||||
stop_config.enabled = false;
|
||||
|
||||
apply_rtsp_config(
|
||||
&state,
|
||||
¤t_config,
|
||||
&stop_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
let stored_config = persist_and_apply(&state, current_config, stop_config).await?;
|
||||
let status = current_status(&state).await;
|
||||
|
||||
let status = {
|
||||
let guard = state.rtsp.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
service.status().await
|
||||
} else {
|
||||
crate::rtsp::RtspServiceStatus::Stopped
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(RtspStatusResponse::new(¤t_config, status)))
|
||||
Ok(Json(RtspStatusResponse::new(&stored_config, status)))
|
||||
}
|
||||
|
||||
@@ -8,9 +8,58 @@ use crate::state::AppState;
|
||||
use super::apply::{apply_rustdesk_config, try_apply_lock, ConfigApplyOptions};
|
||||
use super::types::RustDeskConfigUpdate;
|
||||
|
||||
fn validate_candidate(state: &Arc<AppState>, config: &RustDeskConfig) -> Result<()> {
|
||||
let mut candidate = state.config.get().as_ref().clone();
|
||||
candidate.rustdesk = config.clone();
|
||||
crate::video::codec_constraints::validate_third_party_codec_compatibility(&candidate)
|
||||
}
|
||||
|
||||
async fn persist_and_apply(
|
||||
state: &Arc<AppState>,
|
||||
old_config: RustDeskConfig,
|
||||
new_config: RustDeskConfig,
|
||||
) -> Result<RustDeskConfig> {
|
||||
validate_candidate(state, &new_config)?;
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
config.rustdesk = new_config.clone();
|
||||
})
|
||||
.await?;
|
||||
let stored_config = state.config.get().rustdesk.clone();
|
||||
apply_rustdesk_config(
|
||||
state,
|
||||
&old_config,
|
||||
&stored_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
Ok(stored_config)
|
||||
}
|
||||
|
||||
async fn current_status(state: &Arc<AppState>, config: RustDeskConfig) -> RustDeskStatusResponse {
|
||||
let (service_status, rendezvous_status) = {
|
||||
let guard = state.rustdesk.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
let status = format!("{}", service.status());
|
||||
let rv_status = service.rendezvous_status().map(|s| format!("{}", s));
|
||||
(status, rv_status)
|
||||
} else {
|
||||
("not_initialized".to_string(), None)
|
||||
}
|
||||
};
|
||||
|
||||
RustDeskStatusResponse {
|
||||
config: RustDeskConfigResponse::from(&config),
|
||||
service_status,
|
||||
rendezvous_status,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct RustDeskConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub codec: crate::rustdesk::config::RustDeskCodec,
|
||||
pub rendezvous_server: String,
|
||||
pub relay_server: Option<String>,
|
||||
pub device_id: String,
|
||||
@@ -23,6 +72,7 @@ impl From<&RustDeskConfig> for RustDeskConfigResponse {
|
||||
fn from(config: &RustDeskConfig) -> Self {
|
||||
Self {
|
||||
enabled: config.enabled,
|
||||
codec: config.codec,
|
||||
rendezvous_server: config.rendezvous_server.clone(),
|
||||
relay_server: config.relay_server.clone(),
|
||||
device_id: config.device_id.clone(),
|
||||
@@ -50,23 +100,7 @@ pub async fn get_rustdesk_status(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Json<RustDeskStatusResponse> {
|
||||
let config = state.config.get().rustdesk.clone();
|
||||
|
||||
let (service_status, rendezvous_status) = {
|
||||
let guard = state.rustdesk.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
let status = format!("{}", service.status());
|
||||
let rv_status = service.rendezvous_status().map(|s| format!("{}", s));
|
||||
(status, rv_status)
|
||||
} else {
|
||||
("not_initialized".to_string(), None)
|
||||
}
|
||||
};
|
||||
|
||||
Json(RustDeskStatusResponse {
|
||||
config: RustDeskConfigResponse::from(&config),
|
||||
service_status,
|
||||
rendezvous_status,
|
||||
})
|
||||
Json(current_status(&state, config).await)
|
||||
}
|
||||
|
||||
pub async fn update_rustdesk_config(
|
||||
@@ -81,22 +115,7 @@ pub async fn update_rustdesk_config(
|
||||
req.apply_to(&mut merged_config);
|
||||
req.validate_merged(&merged_config)?;
|
||||
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
config.rustdesk = merged_config.clone();
|
||||
})
|
||||
.await?;
|
||||
|
||||
let new_config = state.config.get().rustdesk.clone();
|
||||
|
||||
apply_rustdesk_config(
|
||||
&state,
|
||||
&old_config,
|
||||
&new_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
let new_config = persist_and_apply(&state, old_config, merged_config).await?;
|
||||
|
||||
let constraints = state.stream_manager.codec_constraints().await;
|
||||
if constraints.rustdesk_enabled || constraints.rtsp_enabled {
|
||||
@@ -152,31 +171,8 @@ pub async fn start_rustdesk_service(
|
||||
let current_config = state.config.get().rustdesk.clone();
|
||||
let mut start_config = current_config.clone();
|
||||
start_config.enabled = true;
|
||||
|
||||
apply_rustdesk_config(
|
||||
&state,
|
||||
¤t_config,
|
||||
&start_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (service_status, rendezvous_status) = {
|
||||
let guard = state.rustdesk.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
let status = format!("{}", service.status());
|
||||
let rv_status = service.rendezvous_status().map(|s| format!("{}", s));
|
||||
(status, rv_status)
|
||||
} else {
|
||||
("not_initialized".to_string(), None)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(RustDeskStatusResponse {
|
||||
config: RustDeskConfigResponse::from(¤t_config),
|
||||
service_status,
|
||||
rendezvous_status,
|
||||
}))
|
||||
let stored_config = persist_and_apply(&state, current_config, start_config).await?;
|
||||
Ok(Json(current_status(&state, stored_config).await))
|
||||
}
|
||||
|
||||
pub async fn stop_rustdesk_service(
|
||||
@@ -187,28 +183,6 @@ pub async fn stop_rustdesk_service(
|
||||
let mut stop_config = current_config.clone();
|
||||
stop_config.enabled = false;
|
||||
|
||||
apply_rustdesk_config(
|
||||
&state,
|
||||
¤t_config,
|
||||
&stop_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (service_status, rendezvous_status) = {
|
||||
let guard = state.rustdesk.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
let status = format!("{}", service.status());
|
||||
let rv_status = service.rendezvous_status().map(|s| format!("{}", s));
|
||||
(status, rv_status)
|
||||
} else {
|
||||
("not_initialized".to_string(), None)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(RustDeskStatusResponse {
|
||||
config: RustDeskConfigResponse::from(¤t_config),
|
||||
service_status,
|
||||
rendezvous_status,
|
||||
}))
|
||||
let stored_config = persist_and_apply(&state, current_config, stop_config).await?;
|
||||
Ok(Json(current_status(&state, stored_config).await))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::config::*;
|
||||
use crate::error::AppError;
|
||||
use crate::rtsp::RtspServiceStatus;
|
||||
use crate::rustdesk::config::RustDeskConfig;
|
||||
use crate::vnc::VncServiceStatus;
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(unix)]
|
||||
@@ -765,6 +766,7 @@ fn validate_rustdesk_relay_key(key: &str) -> Result<(), AppError> {
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RustDeskConfigUpdate {
|
||||
pub enabled: Option<bool>,
|
||||
pub codec: Option<crate::rustdesk::config::RustDeskCodec>,
|
||||
pub rendezvous_server: Option<String>,
|
||||
pub relay_server: Option<String>,
|
||||
pub relay_key: Option<String>,
|
||||
@@ -821,6 +823,9 @@ impl RustDeskConfigUpdate {
|
||||
if let Some(enabled) = self.enabled {
|
||||
config.enabled = enabled;
|
||||
}
|
||||
if let Some(codec) = self.codec {
|
||||
config.codec = codec;
|
||||
}
|
||||
if let Some(ref server) = self.rendezvous_server {
|
||||
config.rendezvous_server = server.clone();
|
||||
}
|
||||
@@ -904,6 +909,125 @@ pub struct RtspConfigUpdate {
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct VncConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub bind: String,
|
||||
pub port: u16,
|
||||
pub encoding: VncEncoding,
|
||||
pub jpeg_quality: u8,
|
||||
pub allow_one_client: bool,
|
||||
pub has_password: bool,
|
||||
}
|
||||
|
||||
impl From<&VncConfig> for VncConfigResponse {
|
||||
fn from(config: &VncConfig) -> Self {
|
||||
Self {
|
||||
enabled: config.enabled,
|
||||
bind: config.bind.clone(),
|
||||
port: config.port,
|
||||
encoding: config.encoding.clone(),
|
||||
jpeg_quality: config.jpeg_quality,
|
||||
allow_one_client: config.allow_one_client,
|
||||
has_password: config.password.as_deref().is_some_and(|p| !p.is_empty()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct VncStatusResponse {
|
||||
pub config: VncConfigResponse,
|
||||
pub service_status: String,
|
||||
pub connection_count: u32,
|
||||
}
|
||||
|
||||
impl VncStatusResponse {
|
||||
pub fn new(config: &VncConfig, status: VncServiceStatus, connection_count: usize) -> Self {
|
||||
Self {
|
||||
config: VncConfigResponse::from(config),
|
||||
service_status: status.to_string(),
|
||||
connection_count: connection_count as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct VncConfigUpdate {
|
||||
pub enabled: Option<bool>,
|
||||
pub bind: Option<String>,
|
||||
pub port: Option<u16>,
|
||||
pub encoding: Option<VncEncoding>,
|
||||
pub jpeg_quality: Option<u8>,
|
||||
pub allow_one_client: Option<bool>,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl VncConfigUpdate {
|
||||
pub fn validate(&self) -> crate::error::Result<()> {
|
||||
if let Some(port) = self.port {
|
||||
if port == 0 {
|
||||
return Err(AppError::BadRequest("VNC port cannot be 0".into()));
|
||||
}
|
||||
}
|
||||
if let Some(ref bind) = self.bind {
|
||||
if bind.parse::<std::net::IpAddr>().is_err() {
|
||||
return Err(AppError::BadRequest("VNC bind must be a valid IP".into()));
|
||||
}
|
||||
}
|
||||
if let Some(quality) = self.jpeg_quality {
|
||||
if !(10..=100).contains(&quality) {
|
||||
return Err(AppError::BadRequest(
|
||||
"VNC JPEG quality must be 10-100".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(ref password) = self.password {
|
||||
if !password.is_empty() && password.len() > 8 {
|
||||
return Err(AppError::BadRequest(
|
||||
"VNCAuth password must be at most 8 characters".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_merged(&self, config: &VncConfig) -> crate::error::Result<()> {
|
||||
if config.enabled && config.password.as_deref().unwrap_or("").is_empty() {
|
||||
return Err(AppError::BadRequest("VNC password is required".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, config: &mut VncConfig) {
|
||||
if let Some(enabled) = self.enabled {
|
||||
config.enabled = enabled;
|
||||
}
|
||||
if let Some(ref bind) = self.bind {
|
||||
config.bind = bind.clone();
|
||||
}
|
||||
if let Some(port) = self.port {
|
||||
config.port = port;
|
||||
}
|
||||
if let Some(ref encoding) = self.encoding {
|
||||
config.encoding = encoding.clone();
|
||||
}
|
||||
if let Some(quality) = self.jpeg_quality {
|
||||
config.jpeg_quality = quality;
|
||||
}
|
||||
if let Some(allow_one_client) = self.allow_one_client {
|
||||
config.allow_one_client = allow_one_client;
|
||||
}
|
||||
if let Some(ref password) = self.password {
|
||||
if !password.is_empty() {
|
||||
config.password = Some(password.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RtspConfigUpdate {
|
||||
pub fn validate(&self) -> crate::error::Result<()> {
|
||||
if let Some(port) = self.port {
|
||||
@@ -1188,6 +1312,7 @@ mod tests {
|
||||
fn rustdesk_relay_key_accepts_hbbs_style_base64_32_bytes() {
|
||||
let update = RustDeskConfigUpdate {
|
||||
enabled: None,
|
||||
codec: None,
|
||||
rendezvous_server: None,
|
||||
relay_server: None,
|
||||
relay_key: Some("pLU0pEj2IZnNVKzrIO1pIdwGA3dOVJJLkFIYGOCGH1E=".to_string()),
|
||||
@@ -1202,6 +1327,7 @@ mod tests {
|
||||
let not_32 = "AAAAAAAAAAAAAAAAAAAAAA==".to_string();
|
||||
let update = RustDeskConfigUpdate {
|
||||
enabled: None,
|
||||
codec: None,
|
||||
rendezvous_server: None,
|
||||
relay_server: None,
|
||||
relay_key: Some(not_32),
|
||||
|
||||
110
src/web/handlers/config/vnc.rs
Normal file
110
src/web/handlers/config/vnc.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use axum::{extract::State, Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::apply::{apply_vnc_config, try_apply_lock, ConfigApplyOptions};
|
||||
use super::types::{VncConfigResponse, VncConfigUpdate, VncStatusResponse};
|
||||
|
||||
fn validate_candidate(state: &Arc<AppState>, config: &crate::config::VncConfig) -> Result<()> {
|
||||
let mut candidate = state.config.get().as_ref().clone();
|
||||
candidate.vnc = config.clone();
|
||||
crate::video::codec_constraints::validate_third_party_codec_compatibility(&candidate)
|
||||
}
|
||||
|
||||
async fn persist_and_apply(
|
||||
state: &Arc<AppState>,
|
||||
old_config: crate::config::VncConfig,
|
||||
new_config: crate::config::VncConfig,
|
||||
) -> Result<crate::config::VncConfig> {
|
||||
validate_candidate(state, &new_config)?;
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
config.vnc = new_config.clone();
|
||||
})
|
||||
.await?;
|
||||
let stored_config = state.config.get().vnc.clone();
|
||||
apply_vnc_config(
|
||||
state,
|
||||
&old_config,
|
||||
&stored_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
Ok(stored_config)
|
||||
}
|
||||
|
||||
async fn current_status(state: &Arc<AppState>) -> (crate::vnc::VncServiceStatus, usize) {
|
||||
let guard = state.vnc.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
(service.status().await, service.connection_count())
|
||||
} else {
|
||||
(crate::vnc::VncServiceStatus::Stopped, 0)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_vnc_config(State(state): State<Arc<AppState>>) -> Json<VncConfigResponse> {
|
||||
Json(VncConfigResponse::from(&state.config.get().vnc))
|
||||
}
|
||||
|
||||
pub async fn get_vnc_status(State(state): State<Arc<AppState>>) -> Json<VncStatusResponse> {
|
||||
let config = state.config.get().vnc.clone();
|
||||
let (status, connection_count) = current_status(&state).await;
|
||||
|
||||
Json(VncStatusResponse::new(&config, status, connection_count))
|
||||
}
|
||||
|
||||
pub async fn update_vnc_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<VncConfigUpdate>,
|
||||
) -> Result<Json<VncConfigResponse>> {
|
||||
req.validate()?;
|
||||
|
||||
let _apply_guard = try_apply_lock(&state.config_apply_locks.vnc, "vnc")?;
|
||||
let old_config = state.config.get().vnc.clone();
|
||||
let mut merged_config = old_config.clone();
|
||||
req.apply_to(&mut merged_config);
|
||||
req.validate_merged(&merged_config)?;
|
||||
let new_config = persist_and_apply(&state, old_config, merged_config).await?;
|
||||
|
||||
Ok(Json(VncConfigResponse::from(&new_config)))
|
||||
}
|
||||
|
||||
pub async fn start_vnc_service(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<VncStatusResponse>> {
|
||||
let _apply_guard = try_apply_lock(&state.config_apply_locks.vnc, "vnc")?;
|
||||
let current_config = state.config.get().vnc.clone();
|
||||
let mut start_config = current_config.clone();
|
||||
start_config.enabled = true;
|
||||
if start_config.password.as_deref().unwrap_or("").is_empty() {
|
||||
start_config.password = current_config.password.clone();
|
||||
}
|
||||
let stored_config = persist_and_apply(&state, current_config, start_config).await?;
|
||||
let (status, connection_count) = current_status(&state).await;
|
||||
|
||||
Ok(Json(VncStatusResponse::new(
|
||||
&stored_config,
|
||||
status,
|
||||
connection_count,
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn stop_vnc_service(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<VncStatusResponse>> {
|
||||
let _apply_guard = try_apply_lock(&state.config_apply_locks.vnc, "vnc")?;
|
||||
let current_config = state.config.get().vnc.clone();
|
||||
let mut stop_config = current_config.clone();
|
||||
stop_config.enabled = false;
|
||||
|
||||
let stored_config = persist_and_apply(&state, current_config, stop_config).await?;
|
||||
|
||||
Ok(Json(VncStatusResponse::new(
|
||||
&stored_config,
|
||||
crate::vnc::VncServiceStatus::Stopped,
|
||||
0,
|
||||
)))
|
||||
}
|
||||
@@ -241,6 +241,7 @@ pub struct StreamConstraintsResponse {
|
||||
pub struct ConstraintSources {
|
||||
pub rustdesk: bool,
|
||||
pub rtsp: bool,
|
||||
pub vnc: bool,
|
||||
}
|
||||
|
||||
/// Get stream codec constraints derived from enabled services.
|
||||
@@ -267,6 +268,7 @@ pub async fn stream_constraints_get(
|
||||
sources: ConstraintSources {
|
||||
rustdesk: constraints.rustdesk_enabled,
|
||||
rtsp: constraints.rtsp_enabled,
|
||||
vnc: constraints.vnc_enabled,
|
||||
},
|
||||
reason: constraints.reason,
|
||||
current_mode,
|
||||
|
||||
@@ -36,6 +36,7 @@ pub struct Capabilities {
|
||||
pub atx: CapabilityInfo,
|
||||
pub audio: CapabilityInfo,
|
||||
pub rustdesk: CapabilityInfo,
|
||||
pub vnc: CapabilityInfo,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -106,6 +107,11 @@ pub async fn system_info(State(state): State<Arc<AppState>>) -> Json<SystemInfo>
|
||||
backend: platform.rustdesk.selected_backend.clone(),
|
||||
reason: platform.rustdesk.reason.clone(),
|
||||
},
|
||||
vnc: CapabilityInfo {
|
||||
available: config.vnc.enabled && platform.vnc.available,
|
||||
backend: platform.vnc.selected_backend.clone(),
|
||||
reason: platform.vnc.reason.clone(),
|
||||
},
|
||||
},
|
||||
disk_space,
|
||||
device_info,
|
||||
|
||||
@@ -143,6 +143,15 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
||||
"/config/rustdesk/stop",
|
||||
post(handlers::config::stop_rustdesk_service),
|
||||
)
|
||||
// VNC configuration endpoints
|
||||
.route("/config/vnc", get(handlers::config::get_vnc_config))
|
||||
.route("/config/vnc", patch(handlers::config::update_vnc_config))
|
||||
.route("/config/vnc/status", get(handlers::config::get_vnc_status))
|
||||
.route(
|
||||
"/config/vnc/start",
|
||||
post(handlers::config::start_vnc_service),
|
||||
)
|
||||
.route("/config/vnc/stop", post(handlers::config::stop_vnc_service))
|
||||
// RTSP configuration endpoints
|
||||
.route("/config/rtsp", get(handlers::config::get_rtsp_config))
|
||||
.route("/config/rtsp", patch(handlers::config::update_rtsp_config))
|
||||
|
||||
Reference in New Issue
Block a user