From d35021ee5d0ae25b3282c1ff261c49605a0df517 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Fri, 17 Jul 2026 10:46:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=BC=80=E5=90=AF=E5=92=8C=E8=87=AA=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E7=8A=B6=E6=80=81=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.rs | 6 +- src/rtsp/service.rs | 83 ++++++-- src/rustdesk/mod.rs | 36 +++- src/state.rs | 27 +++ src/video/stream_manager.rs | 13 +- src/vnc/mod.rs | 44 ++-- src/web/handlers/config/apply.rs | 234 ++++++++++++++++------ src/web/handlers/config/rtsp.rs | 29 ++- src/web/handlers/config/rustdesk.rs | 30 ++- src/web/handlers/config/vnc.rs | 31 ++- web/src/components/VideoConfigPopover.vue | 15 +- web/src/views/SettingsView.vue | 18 +- 12 files changed, 430 insertions(+), 136 deletions(-) diff --git a/src/main.rs b/src/main.rs index 86110487..ee9a9026 100644 --- a/src/main.rs +++ b/src/main.rs @@ -643,8 +643,12 @@ async fn main() -> anyhow::Result<()> { } { - let runtime_config = state.config.get(); + let runtime_config = state.runtime_third_party_config().await; let constraints = StreamCodecConstraints::from_config(&runtime_config); + state + .stream_manager + .set_runtime_codec_constraints(constraints.clone()) + .await; match enforce_constraints_with_stream_manager(&state.stream_manager, &constraints).await { Ok(result) if result.changed => { if let Some(message) = result.message { diff --git a/src/rtsp/service.rs b/src/rtsp/service.rs index 0e0897cc..6cf2a37e 100644 --- a/src/rtsp/service.rs +++ b/src/rtsp/service.rs @@ -2,9 +2,11 @@ use rtsp_types as rtsp; use std::io; use std::net::SocketAddr; use std::sync::Arc; +use std::time::Duration; use tokio::io::AsyncReadExt; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::{broadcast, Mutex, RwLock}; +use tokio::task::JoinHandle; use crate::config::RtspConfig; use crate::error::{AppError, Result}; @@ -25,6 +27,8 @@ use super::types::RtspConnectionState; pub use super::types::RtspServiceStatus; +const RTSP_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(2); + pub struct RtspService { config: Arc>, status: Arc>, @@ -74,18 +78,37 @@ impl RtspService { tracing::debug!("Failed to request keyframe on RTSP start: {}", err); } - let bind_addr = bind_socket_addr(&config.bind, config.port) - .map_err(|e| AppError::BadRequest(format!("Invalid RTSP bind address: {}", e)))?; + let bind_addr = match bind_socket_addr(&config.bind, config.port) { + Ok(addr) => addr, + Err(err) => { + let error = AppError::BadRequest(format!("Invalid RTSP bind address: {}", err)); + *self.status.write().await = RtspServiceStatus::Error(error.to_string()); + return Err(error); + } + }; - let listener = bind_tcp_listener(bind_addr).map_err(|e| { - AppError::Io(io::Error::new(e.kind(), format!("RTSP bind failed: {}", e))) - })?; - let listener = TcpListener::from_std(listener).map_err(|e| { - AppError::Io(io::Error::new( - e.kind(), - format!("RTSP listener setup failed: {}", e), - )) - })?; + let listener = match bind_tcp_listener(bind_addr) { + Ok(listener) => listener, + Err(err) => { + let error = AppError::Io(io::Error::new( + err.kind(), + format!("RTSP bind failed: {}", err), + )); + *self.status.write().await = RtspServiceStatus::Error(error.to_string()); + return Err(error); + } + }; + let listener = match TcpListener::from_std(listener) { + Ok(listener) => listener, + Err(err) => { + let error = AppError::Io(io::Error::new( + err.kind(), + format!("RTSP listener setup failed: {}", err), + )); + *self.status.write().await = RtspServiceStatus::Error(error.to_string()); + return Err(error); + } + }; let service_config = self.config.clone(); let video_manager = self.video_manager.clone(); @@ -138,7 +161,7 @@ impl RtspService { pub async fn stop(&self) -> Result<()> { let _ = self.shutdown_tx.send(()); if let Some(handle) = self.server_handle.lock().await.take() { - handle.abort(); + wait_for_server_stop(handle).await; } let mut client_handles = self.client_handles.lock().await; @@ -170,6 +193,42 @@ impl RtspService { } } +async fn wait_for_server_stop(mut handle: JoinHandle<()>) { + match tokio::time::timeout(RTSP_SHUTDOWN_TIMEOUT, &mut handle).await { + Ok(Ok(())) => {} + Ok(Err(err)) if err.is_cancelled() => {} + Ok(Err(err)) => tracing::warn!("RTSP server task ended with error: {}", err), + Err(_) => { + tracing::warn!("Timed out waiting for RTSP server task to stop"); + handle.abort(); + let _ = handle.await; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn waiting_for_server_stop_releases_listener() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind test listener"); + let bind_addr = listener.local_addr().expect("read test listener address"); + let (shutdown_tx, mut shutdown_rx) = broadcast::channel(1); + let handle = tokio::spawn(async move { + let _listener = listener; + let _ = shutdown_rx.recv().await; + }); + + shutdown_tx.send(()).expect("send shutdown signal"); + wait_for_server_stop(handle).await; + + std::net::TcpListener::bind(bind_addr).expect("rebind released listener address"); + } +} + async fn handle_client( mut stream: TcpStream, peer: SocketAddr, diff --git a/src/rustdesk/mod.rs b/src/rustdesk/mod.rs index 2a1f6b49..cd17cef0 100644 --- a/src/rustdesk/mod.rs +++ b/src/rustdesk/mod.rs @@ -32,6 +32,7 @@ use self::protocol::{make_local_addr, make_relay_response, make_request_relay}; use self::rendezvous::{AddrMangle, RendezvousMediator, RendezvousStatus}; const RELAY_CONNECT_TIMEOUT_MS: u64 = 10_000; +const SERVICE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(2); #[derive(Debug, Clone, PartialEq)] pub enum ServiceStatus { @@ -121,6 +122,10 @@ impl RustDeskService { self.connection_manager.connection_count() } + pub fn is_listening(&self) -> bool { + self.tcp_listener_handle.read().is_some() + } + pub async fn start(&self) -> anyhow::Result<()> { let config = self.config.read().clone(); @@ -169,7 +174,13 @@ impl RustDeskService { *self.rendezvous.write() = Some(mediator.clone()); - let (tcp_handles, listen_port) = self.start_tcp_listener_with_port().await?; + let (tcp_handles, listen_port) = match self.start_tcp_listener_with_port().await { + Ok(result) => result, + Err(err) => { + *self.status.write() = ServiceStatus::Error(err.to_string()); + return Err(err); + } + }; *self.tcp_listener_handle.write() = Some(tcp_handles); mediator.set_listen_port(listen_port); @@ -383,13 +394,15 @@ impl RustDeskService { mediator.stop(); } - if let Some(handle) = self.rendezvous_handle.write().take() { - handle.abort(); + let rendezvous_handle = self.rendezvous_handle.write().take(); + if let Some(handle) = rendezvous_handle { + wait_for_service_task(handle, "rendezvous").await; } - if let Some(handles) = self.tcp_listener_handle.write().take() { + let tcp_listener_handles = self.tcp_listener_handle.write().take(); + if let Some(handles) = tcp_listener_handles { for handle in handles { - handle.abort(); + wait_for_service_task(handle, "TCP listener").await; } } @@ -454,6 +467,19 @@ impl RustDeskService { } } +async fn wait_for_service_task(mut handle: JoinHandle<()>, task_name: &str) { + match tokio::time::timeout(SERVICE_SHUTDOWN_TIMEOUT, &mut handle).await { + Ok(Ok(())) => {} + Ok(Err(err)) if err.is_cancelled() => {} + Ok(Err(err)) => warn!("RustDesk {} task ended with error: {}", task_name, err), + Err(_) => { + warn!("Timed out waiting for RustDesk {} task to stop", task_name); + handle.abort(); + let _ = handle.await; + } + } +} + fn rustdesk_relay_key(config: &Arc>) -> String { config.read().relay_key.clone().unwrap_or_default() } diff --git a/src/state.rs b/src/state.rs index 2ac77c19..e554451b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -150,6 +150,33 @@ impl AppState { &self.data_dir } + pub async fn runtime_third_party_config(&self) -> crate::config::AppConfig { + let mut config = self.config.get().as_ref().clone(); + + config.rustdesk.enabled = self + .rustdesk + .read() + .await + .as_ref() + .is_some_and(|service| service.is_listening()); + config.vnc.enabled = match self.vnc.read().await.as_ref() { + Some(service) => matches!( + service.status().await, + crate::vnc::VncServiceStatus::Starting | crate::vnc::VncServiceStatus::Running + ), + None => false, + }; + config.rtsp.enabled = match self.rtsp.read().await.as_ref() { + Some(service) => matches!( + service.status().await, + crate::rtsp::RtspServiceStatus::Starting | crate::rtsp::RtspServiceStatus::Running + ), + None => false, + }; + + config + } + pub fn subscribe_device_info(&self) -> watch::Receiver> { self.device_info_tx.subscribe() } diff --git a/src/video/stream_manager.rs b/src/video/stream_manager.rs index d591cfc8..d60e4b7d 100644 --- a/src/video/stream_manager.rs +++ b/src/video/stream_manager.rs @@ -100,6 +100,8 @@ pub struct VideoStreamManager { events: RwLock>>, /// Configuration store config_store: RwLock>, + /// Codec constraints derived from services that are actually running. + runtime_codec_constraints: RwLock>, /// Mode switching lock to prevent concurrent switch requests switching: AtomicBool, /// Current mode switch transaction ID (set while switching=true) @@ -118,6 +120,7 @@ impl VideoStreamManager { webrtc_streamer, events: RwLock::new(None), config_store: RwLock::new(None), + runtime_codec_constraints: RwLock::new(None), switching: AtomicBool::new(false), transition_id: RwLock::new(None), }) @@ -144,8 +147,16 @@ impl VideoStreamManager { *self.config_store.write().await = Some(config); } - /// Get current stream codec constraints derived from global configuration. + pub async fn set_runtime_codec_constraints(&self, constraints: StreamCodecConstraints) { + *self.runtime_codec_constraints.write().await = Some(constraints); + } + + /// Get current stream codec constraints derived from running services. pub async fn codec_constraints(&self) -> StreamCodecConstraints { + if let Some(constraints) = self.runtime_codec_constraints.read().await.as_ref() { + return constraints.clone(); + } + if let Some(ref config_store) = *self.config_store.read().await { let config = config_store.get(); StreamCodecConstraints::from_config(&config) diff --git a/src/vnc/mod.rs b/src/vnc/mod.rs index 96d4e53d..c7395482 100644 --- a/src/vnc/mod.rs +++ b/src/vnc/mod.rs @@ -121,20 +121,36 @@ impl VncService { return Err(err); } - let bind_addr = bind_socket_addr(&config.bind, config.port) - .map_err(|e| AppError::BadRequest(format!("Invalid VNC bind address: {}", e)))?; - let listener = bind_tcp_listener(bind_addr).map_err(|e| { - AppError::Io(std::io::Error::new( - e.kind(), - format!("VNC bind failed: {}", e), - )) - })?; - let listener = TcpListener::from_std(listener).map_err(|e| { - AppError::Io(std::io::Error::new( - e.kind(), - format!("VNC listener setup failed: {}", e), - )) - })?; + let bind_addr = match bind_socket_addr(&config.bind, config.port) { + Ok(addr) => addr, + Err(err) => { + let error = AppError::BadRequest(format!("Invalid VNC bind address: {}", err)); + *self.status.write().await = VncServiceStatus::Error(error.to_string()); + return Err(error); + } + }; + let listener = match bind_tcp_listener(bind_addr) { + Ok(listener) => listener, + Err(err) => { + let error = AppError::Io(std::io::Error::new( + err.kind(), + format!("VNC bind failed: {}", err), + )); + *self.status.write().await = VncServiceStatus::Error(error.to_string()); + return Err(error); + } + }; + let listener = match TcpListener::from_std(listener) { + Ok(listener) => listener, + Err(err) => { + let error = AppError::Io(std::io::Error::new( + err.kind(), + format!("VNC listener setup failed: {}", err), + )); + *self.status.write().await = VncServiceStatus::Error(error.to_string()); + return Err(error); + } + }; let config_ref = self.config.clone(); let video_manager = self.video_manager.clone(); diff --git a/src/web/handlers/config/apply.rs b/src/web/handlers/config/apply.rs index 35ff1e7e..0ea6e4fa 100644 --- a/src/web/handlers/config/apply.rs +++ b/src/web/handlers/config/apply.rs @@ -14,11 +14,33 @@ use tokio::sync::{Mutex, OwnedMutexGuard}; #[derive(Debug, Clone, Copy, Default)] pub struct ConfigApplyOptions { pub force: bool, + pub preserve_service_state: bool, + pub runtime_only: bool, } impl ConfigApplyOptions { pub const fn forced() -> Self { - Self { force: true } + Self { + force: true, + preserve_service_state: false, + runtime_only: false, + } + } + + pub const fn preserving_service_state() -> Self { + Self { + force: false, + preserve_service_state: true, + runtime_only: false, + } + } + + pub const fn runtime_only() -> Self { + Self { + force: false, + preserve_service_state: false, + runtime_only: true, + } } } @@ -449,13 +471,27 @@ pub async fn apply_audio_config( } pub async fn enforce_stream_codec_constraints(state: &Arc) -> Result> { - let config = state.config.get(); + let config = state.runtime_third_party_config().await; let constraints = StreamCodecConstraints::from_config(&config); + state + .stream_manager + .set_runtime_codec_constraints(constraints.clone()) + .await; let enforcement = enforce_constraints_with_stream_manager(&state.stream_manager, &constraints).await?; Ok(enforcement.message) } +async fn validate_runtime_candidate( + state: &Arc, + apply: impl FnOnce(&mut crate::config::AppConfig, T), + config: T, +) -> Result<()> { + let mut candidate = state.runtime_third_party_config().await; + apply(&mut candidate, config); + validate_third_party_codec_compatibility(&candidate) +} + fn validate_rustdesk_candidate( state: &Arc, new_config: &crate::rustdesk::config::RustDeskConfig, @@ -485,12 +521,26 @@ pub async fn apply_rustdesk_config( ) -> Result<()> { tracing::info!("Applying RustDesk config changes..."); - validate_rustdesk_candidate(state, new_config)?; + if options.runtime_only { + validate_runtime_candidate( + state, + |candidate, config| candidate.rustdesk = config, + new_config.clone(), + ) + .await?; + } else { + validate_rustdesk_candidate(state, new_config)?; + } let mut rustdesk_guard = state.rustdesk.write().await; let mut credentials_to_save = None; + 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; - if !new_config.enabled { + if !options.preserve_service_state && !new_config.enabled { if let Some(ref service) = *rustdesk_guard { service .stop() @@ -501,39 +551,51 @@ pub async fn apply_rustdesk_config( *rustdesk_guard = None; } - 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; - + if !options.preserve_service_state && new_config.enabled { if rustdesk_guard.is_none() { tracing::info!("Initializing RustDesk service..."); - let service = crate::rustdesk::RustDeskService::new( + let service = std::sync::Arc::new(crate::rustdesk::RustDeskService::new( new_config.clone(), state.stream_manager.clone(), state.hid.clone(), state.audio.clone(), - ); + )); + *rustdesk_guard = Some(service.clone()); service.start().await.map_err(|e| { AppError::Config(format!("Failed to start RustDesk service: {}", e)) })?; tracing::info!("RustDesk service started with ID: {}", new_config.device_id); credentials_to_save = service.save_credentials(); - *rustdesk_guard = Some(std::sync::Arc::new(service)); - } else if need_restart { + } else { if let Some(ref service) = *rustdesk_guard { - service.restart(new_config.clone()).await.map_err(|e| { - AppError::Config(format!("Failed to restart RustDesk service: {}", e)) - })?; - tracing::info!( - "RustDesk service restarted with ID: {}", - new_config.device_id - ); + if service.is_listening() { + if need_restart { + service.restart(new_config.clone()).await.map_err(|e| { + AppError::Config(format!("Failed to restart RustDesk service: {}", e)) + })?; + tracing::info!( + "RustDesk service restarted with ID: {}", + new_config.device_id + ); + } + } else { + service.update_config(new_config.clone()); + service.start().await.map_err(|e| { + AppError::Config(format!("Failed to start RustDesk service: {}", e)) + })?; + } credentials_to_save = service.save_credentials(); } } + } else if options.preserve_service_state && need_restart { + if let Some(ref service) = *rustdesk_guard { + let mut runtime_config = new_config.clone(); + runtime_config.enabled = true; + service.restart(runtime_config).await.map_err(|e| { + AppError::Config(format!("Failed to restart RustDesk service: {}", e)) + })?; + credentials_to_save = service.save_credentials(); + } } drop(rustdesk_guard); @@ -567,11 +629,27 @@ pub async fn apply_vnc_config( ) -> Result<()> { tracing::info!("Applying VNC config changes..."); - validate_vnc_candidate(state, new_config)?; + if options.runtime_only { + validate_runtime_candidate( + state, + |candidate, config| candidate.vnc = config, + new_config.clone(), + ) + .await?; + } else { + validate_vnc_candidate(state, new_config)?; + } - if new_config.enabled { - let mut candidate = state.config.get().as_ref().clone(); + let runtime_config = state.runtime_third_party_config().await; + let will_run = if options.preserve_service_state { + runtime_config.vnc.enabled + } else { + new_config.enabled + }; + if will_run { + let mut candidate = runtime_config; candidate.vnc = new_config.clone(); + candidate.vnc.enabled = true; let constraints = StreamCodecConstraints::from_config(&candidate); match enforce_constraints_with_stream_manager(&state.stream_manager, &constraints).await { Ok(result) if result.changed => { @@ -588,37 +666,52 @@ pub async fn apply_vnc_config( } let mut vnc_guard = state.vnc.write().await; + 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.allow_one_client != new_config.allow_one_client; - if !new_config.enabled { + if !options.preserve_service_state && !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.allow_one_client != new_config.allow_one_client; - + if !options.preserve_service_state && new_config.enabled { if vnc_guard.is_none() { - let service = crate::vnc::VncService::new( + let service = Arc::new(crate::vnc::VncService::new( new_config.clone(), state.stream_manager.clone(), state.hid.clone(), - ); + )); + *vnc_guard = Some(service.clone()); service.start().await?; - *vnc_guard = Some(Arc::new(service)); tracing::info!("VNC service started"); - } else if need_restart { + } else { if let Some(ref service) = *vnc_guard { - service.restart(new_config.clone()).await?; - tracing::info!("VNC service restarted"); + if matches!( + service.status().await, + crate::vnc::VncServiceStatus::Running + ) { + if need_restart { + service.restart(new_config.clone()).await?; + tracing::info!("VNC service restarted"); + } + } else { + service.update_config(new_config.clone()).await; + service.start().await?; + } } } + } else if options.preserve_service_state && need_restart { + if let Some(ref service) = *vnc_guard { + let mut runtime_config = new_config.clone(); + runtime_config.enabled = true; + service.restart(runtime_config).await?; + } } drop(vnc_guard); @@ -637,11 +730,28 @@ pub async fn apply_rtsp_config( ) -> Result<()> { tracing::info!("Applying RTSP config changes..."); - validate_rtsp_candidate(state, new_config)?; + if options.runtime_only { + validate_runtime_candidate( + state, + |candidate, config| candidate.rtsp = config, + new_config.clone(), + ) + .await?; + } else { + validate_rtsp_candidate(state, new_config)?; + } let mut rtsp_guard = state.rtsp.write().await; + let need_restart = options.force + || old_config.bind != new_config.bind + || old_config.port != new_config.port + || old_config.path != new_config.path + || old_config.codec != new_config.codec + || old_config.username != new_config.username + || old_config.password != new_config.password + || old_config.allow_one_client != new_config.allow_one_client; - if !new_config.enabled { + if !options.preserve_service_state && !new_config.enabled { if let Some(ref service) = *rtsp_guard { service .stop() @@ -651,27 +761,37 @@ pub async fn apply_rtsp_config( *rtsp_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.path != new_config.path - || old_config.codec != new_config.codec - || old_config.username != new_config.username - || old_config.password != new_config.password - || old_config.allow_one_client != new_config.allow_one_client; - + if !options.preserve_service_state && new_config.enabled { if rtsp_guard.is_none() { - let service = RtspService::new(new_config.clone(), state.stream_manager.clone()); + let service = Arc::new(RtspService::new( + new_config.clone(), + state.stream_manager.clone(), + )); + *rtsp_guard = Some(service.clone()); service.start().await?; tracing::info!("RTSP service started"); - *rtsp_guard = Some(Arc::new(service)); - } else if need_restart { + } else { if let Some(ref service) = *rtsp_guard { - service.restart(new_config.clone()).await?; - tracing::info!("RTSP service restarted"); + if matches!( + service.status().await, + crate::rtsp::RtspServiceStatus::Running + ) { + if need_restart { + service.restart(new_config.clone()).await?; + tracing::info!("RTSP service restarted"); + } + } else { + service.update_config(new_config.clone()).await; + service.start().await?; + } } } + } else if options.preserve_service_state && need_restart { + if let Some(ref service) = *rtsp_guard { + let mut runtime_config = new_config.clone(); + runtime_config.enabled = true; + service.restart(runtime_config).await?; + } } drop(rtsp_guard); diff --git a/src/web/handlers/config/rtsp.rs b/src/web/handlers/config/rtsp.rs index e2d505a0..7a5a5fbf 100644 --- a/src/web/handlers/config/rtsp.rs +++ b/src/web/handlers/config/rtsp.rs @@ -30,7 +30,7 @@ async fn persist_and_apply( state, &old_config, &stored_config, - ConfigApplyOptions::forced(), + ConfigApplyOptions::preserving_service_state(), ) .await?; Ok(stored_config) @@ -76,10 +76,17 @@ pub async fn start_rtsp_service( State(state): State>, ) -> Result> { let _apply_guard = try_apply_lock(&state.config_apply_locks.rtsp, "rtsp")?; - let current_config = state.config.get().rtsp.clone(); - let mut start_config = current_config.clone(); + let stored_config = state.config.get().rtsp.clone(); + let runtime_config = state.runtime_third_party_config().await.rtsp; + let mut start_config = stored_config.clone(); start_config.enabled = true; - let stored_config = persist_and_apply(&state, current_config, start_config).await?; + apply_rtsp_config( + &state, + &runtime_config, + &start_config, + ConfigApplyOptions::runtime_only(), + ) + .await?; let status = current_status(&state).await; Ok(Json(RtspStatusResponse::new(&stored_config, status))) @@ -89,11 +96,17 @@ pub async fn stop_rtsp_service( State(state): State>, ) -> Result> { let _apply_guard = try_apply_lock(&state.config_apply_locks.rtsp, "rtsp")?; - let current_config = state.config.get().rtsp.clone(); - let mut stop_config = current_config.clone(); + let stored_config = state.config.get().rtsp.clone(); + let runtime_config = state.runtime_third_party_config().await.rtsp; + let mut stop_config = stored_config.clone(); stop_config.enabled = false; - - let stored_config = persist_and_apply(&state, current_config, stop_config).await?; + apply_rtsp_config( + &state, + &runtime_config, + &stop_config, + ConfigApplyOptions::runtime_only(), + ) + .await?; let status = current_status(&state).await; Ok(Json(RtspStatusResponse::new(&stored_config, status))) diff --git a/src/web/handlers/config/rustdesk.rs b/src/web/handlers/config/rustdesk.rs index e3ce3cff..ce08d920 100644 --- a/src/web/handlers/config/rustdesk.rs +++ b/src/web/handlers/config/rustdesk.rs @@ -31,7 +31,7 @@ async fn persist_and_apply( state, &old_config, &stored_config, - ConfigApplyOptions::forced(), + ConfigApplyOptions::preserving_service_state(), ) .await?; Ok(stored_config) @@ -168,10 +168,18 @@ pub async fn start_rustdesk_service( State(state): State>, ) -> Result> { let _apply_guard = try_apply_lock(&state.config_apply_locks.rustdesk, "rustdesk")?; - let current_config = state.config.get().rustdesk.clone(); - let mut start_config = current_config.clone(); + let stored_config = state.config.get().rustdesk.clone(); + let runtime_config = state.runtime_third_party_config().await.rustdesk; + let mut start_config = stored_config.clone(); start_config.enabled = true; - let stored_config = persist_and_apply(&state, current_config, start_config).await?; + apply_rustdesk_config( + &state, + &runtime_config, + &start_config, + ConfigApplyOptions::runtime_only(), + ) + .await?; + let stored_config = state.config.get().rustdesk.clone(); Ok(Json(current_status(&state, stored_config).await)) } @@ -179,10 +187,16 @@ pub async fn stop_rustdesk_service( State(state): State>, ) -> Result> { let _apply_guard = try_apply_lock(&state.config_apply_locks.rustdesk, "rustdesk")?; - let current_config = state.config.get().rustdesk.clone(); - let mut stop_config = current_config.clone(); + let stored_config = state.config.get().rustdesk.clone(); + let runtime_config = state.runtime_third_party_config().await.rustdesk; + let mut stop_config = stored_config.clone(); stop_config.enabled = false; - - let stored_config = persist_and_apply(&state, current_config, stop_config).await?; + apply_rustdesk_config( + &state, + &runtime_config, + &stop_config, + ConfigApplyOptions::runtime_only(), + ) + .await?; Ok(Json(current_status(&state, stored_config).await)) } diff --git a/src/web/handlers/config/vnc.rs b/src/web/handlers/config/vnc.rs index 8945df7b..f68a8b42 100644 --- a/src/web/handlers/config/vnc.rs +++ b/src/web/handlers/config/vnc.rs @@ -30,7 +30,7 @@ async fn persist_and_apply( state, &old_config, &stored_config, - ConfigApplyOptions::forced(), + ConfigApplyOptions::preserving_service_state(), ) .await?; Ok(stored_config) @@ -76,13 +76,20 @@ pub async fn start_vnc_service( State(state): State>, ) -> Result> { 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(); + let stored_config = state.config.get().vnc.clone(); + let runtime_config = state.runtime_third_party_config().await.vnc; + let mut start_config = stored_config.clone(); start_config.enabled = true; if start_config.password.as_deref().unwrap_or("").is_empty() { - start_config.password = current_config.password.clone(); + start_config.password = stored_config.password.clone(); } - let stored_config = persist_and_apply(&state, current_config, start_config).await?; + apply_vnc_config( + &state, + &runtime_config, + &start_config, + ConfigApplyOptions::runtime_only(), + ) + .await?; let (status, connection_count) = current_status(&state).await; Ok(Json(VncStatusResponse::new( @@ -96,11 +103,17 @@ pub async fn stop_vnc_service( State(state): State>, ) -> Result> { 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(); + let stored_config = state.config.get().vnc.clone(); + let runtime_config = state.runtime_third_party_config().await.vnc; + let mut stop_config = stored_config.clone(); stop_config.enabled = false; - - let stored_config = persist_and_apply(&state, current_config, stop_config).await?; + apply_vnc_config( + &state, + &runtime_config, + &stop_config, + ConfigApplyOptions::runtime_only(), + ) + .await?; Ok(Json(VncStatusResponse::new( &stored_config, diff --git a/web/src/components/VideoConfigPopover.vue b/web/src/components/VideoConfigPopover.vue index 3d9a0402..445545e3 100644 --- a/web/src/components/VideoConfigPopover.vue +++ b/web/src/components/VideoConfigPopover.vue @@ -74,18 +74,9 @@ const loadingCodecs = ref(false) const backends = ref([]) const constraints = ref(null) const currentEncoderBackend = computed(() => configStore.stream?.encoder || 'auto') -const isRtspEnabled = computed(() => { - if (typeof configStore.rtspStatus?.config?.enabled === 'boolean') { - return configStore.rtspStatus.config.enabled - } - return !!configStore.rtspConfig?.enabled -}) -const isRustdeskEnabled = computed(() => { - if (typeof configStore.rustdeskStatus?.config?.enabled === 'boolean') { - return configStore.rustdeskStatus.config.enabled - } - return !!configStore.rustdeskConfig?.enabled -}) +const isServiceActive = (status: string | undefined) => status === 'starting' || status === 'running' +const isRtspEnabled = computed(() => isServiceActive(configStore.rtspStatus?.service_status)) +const isRustdeskEnabled = computed(() => isServiceActive(configStore.rustdeskStatus?.service_status)) const isRtspCodecLocked = computed(() => isRtspEnabled.value) const isRustdeskWebrtcLocked = computed(() => !isRtspEnabled.value && isRustdeskEnabled.value) const codecLockSources = computed(() => { diff --git a/web/src/views/SettingsView.vue b/web/src/views/SettingsView.vue index 54a9f801..6d686cba 100644 --- a/web/src/views/SettingsView.vue +++ b/web/src/views/SettingsView.vue @@ -2376,7 +2376,7 @@ async function startRustdesk() { rustdeskLoading.value = true try { - await configStore.updateRustdesk(rustdeskUpdatePayload(true)) + await configStore.updateRustdesk(rustdeskUpdatePayload()) const status = await rustdeskConfigApi.start() applyRustdeskStatus(status) } catch { @@ -2501,7 +2501,7 @@ async function saveRtspConfig() { async function startRtsp() { rtspLoading.value = true try { - await configStore.updateRtsp(rtspUpdatePayload(true)) + await configStore.updateRtsp(rtspUpdatePayload()) const status = await rtspConfigApi.start() applyRtspStatus(status) } catch { @@ -2578,7 +2578,7 @@ async function startVnc() { vncLoading.value = true try { - await configStore.updateVnc(vncUpdatePayload(true)) + await configStore.updateVnc(vncUpdatePayload()) const status = await vncConfigApi.start() applyVncStatus(status) } catch { @@ -4714,7 +4714,7 @@ watch(isWindows, () => {
- +
@@ -4781,7 +4781,7 @@ watch(isWindows, () => { {{ t('extensions.rtsp.urlPreview') }} {{ rtspStreamUrl }}
- @@ -4844,7 +4844,7 @@ watch(isWindows, () => {
- +
@@ -4903,7 +4903,7 @@ watch(isWindows, () => { {{ t('extensions.vnc.urlPreview') }} {{ vncStreamUrl }}
- @@ -4969,7 +4969,7 @@ watch(isWindows, () => {
- +
@@ -5093,7 +5093,7 @@ watch(isWindows, () => {
-