feat: 新增 MJPEG/H.264 VNC 初步支持

This commit is contained in:
mofeng-git
2026-06-15 22:23:27 +08:00
parent 5c98aea7e3
commit c101ef1c80
34 changed files with 2270 additions and 354 deletions

View File

@@ -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 {

View File

@@ -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> {

View File

@@ -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,
&current_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(&current_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,
&current_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(&current_config, status)))
Ok(Json(RtspStatusResponse::new(&stored_config, status)))
}

View File

@@ -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,
&current_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(&current_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,
&current_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(&current_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))
}

View File

@@ -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),

View 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,
)))
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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))