feat: 支持 rtsp 功能

This commit is contained in:
mofeng-git
2026-02-11 16:06:06 +08:00
parent 261deb1303
commit 3824e57fc5
23 changed files with 2154 additions and 37 deletions

View File

@@ -92,6 +92,8 @@ arc-swap = "1.8"
# WebRTC # WebRTC
webrtc = "0.14" webrtc = "0.14"
rtp = "0.14" rtp = "0.14"
rtsp-types = "0.1"
sdp-types = "0.1"
# Audio (ALSA capture + Opus encoding) # Audio (ALSA capture + Opus encoding)
# Note: audiopus links to libopus.so (unavoidable for audio support) # Note: audiopus links to libopus.so (unavoidable for audio support)

View File

@@ -35,6 +35,8 @@ pub struct AppConfig {
pub extensions: ExtensionsConfig, pub extensions: ExtensionsConfig,
/// RustDesk remote access settings /// RustDesk remote access settings
pub rustdesk: RustDeskConfig, pub rustdesk: RustDeskConfig,
/// RTSP streaming settings
pub rtsp: RtspConfig,
} }
@@ -404,6 +406,56 @@ pub enum StreamMode {
Mjpeg, Mjpeg,
} }
/// RTSP output codec
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum RtspCodec {
#[default]
H264,
H265,
}
/// RTSP configuration
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct RtspConfig {
/// Enable RTSP output
pub enabled: bool,
/// Bind IP address
pub bind: String,
/// RTSP TCP listen port
pub port: u16,
/// Stream path (without leading slash)
pub path: String,
/// Allow only one client connection at a time
pub allow_one_client: bool,
/// Output codec (H264/H265)
pub codec: RtspCodec,
/// Optional username for authentication
pub username: Option<String>,
/// Optional password for authentication
#[typeshare(skip)]
pub password: Option<String>,
}
impl Default for RtspConfig {
fn default() -> Self {
Self {
enabled: false,
bind: "0.0.0.0".to_string(),
port: 8554,
path: "live".to_string(),
allow_one_client: true,
codec: RtspCodec::H264,
username: None,
password: None,
}
}
}
/// Encoder type /// Encoder type
#[typeshare] #[typeshare]

View File

@@ -14,6 +14,7 @@ pub mod hid;
pub mod modules; pub mod modules;
pub mod msd; pub mod msd;
pub mod otg; pub mod otg;
pub mod rtsp;
pub mod rustdesk; pub mod rustdesk;
pub mod state; pub mod state;
pub mod stream; pub mod stream;

View File

@@ -19,9 +19,13 @@ use one_kvm::extensions::ExtensionManager;
use one_kvm::hid::{HidBackendType, HidController}; use one_kvm::hid::{HidBackendType, HidController};
use one_kvm::msd::MsdController; use one_kvm::msd::MsdController;
use one_kvm::otg::{configfs, OtgService}; use one_kvm::otg::{configfs, OtgService};
use one_kvm::rtsp::RtspService;
use one_kvm::rustdesk::RustDeskService; use one_kvm::rustdesk::RustDeskService;
use one_kvm::state::AppState; use one_kvm::state::AppState;
use one_kvm::utils::bind_tcp_listener; use one_kvm::utils::bind_tcp_listener;
use one_kvm::video::codec_constraints::{
enforce_constraints_with_stream_manager, StreamCodecConstraints,
};
use one_kvm::video::format::{PixelFormat, Resolution}; use one_kvm::video::format::{PixelFormat, Resolution};
use one_kvm::video::{Streamer, VideoStreamManager}; use one_kvm::video::{Streamer, VideoStreamManager};
use one_kvm::web; use one_kvm::web;
@@ -534,6 +538,21 @@ async fn main() -> anyhow::Result<()> {
None None
}; };
// Create RTSP service (optional, based on config)
let rtsp = if config.rtsp.enabled {
tracing::info!(
"Initializing RTSP service: rtsp://{}:{}/{}",
config.rtsp.bind,
config.rtsp.port,
config.rtsp.path
);
let service = RtspService::new(config.rtsp.clone(), stream_manager.clone());
Some(Arc::new(service))
} else {
tracing::info!("RTSP disabled in configuration");
None
};
// Create application state // Create application state
let state = AppState::new( let state = AppState::new(
config_store.clone(), config_store.clone(),
@@ -546,6 +565,7 @@ async fn main() -> anyhow::Result<()> {
atx, atx,
audio, audio,
rustdesk.clone(), rustdesk.clone(),
rtsp.clone(),
extensions.clone(), extensions.clone(),
events.clone(), events.clone(),
shutdown_tx.clone(), shutdown_tx.clone(),
@@ -577,6 +597,30 @@ async fn main() -> anyhow::Result<()> {
} }
} }
// Start RTSP service if enabled
if let Some(ref service) = rtsp {
if let Err(e) = service.start().await {
tracing::error!("Failed to start RTSP service: {}", e);
} else {
tracing::info!("RTSP service started");
}
}
// Enforce startup codec constraints (e.g. RTSP/RustDesk locks)
{
let runtime_config = state.config.get();
let constraints = StreamCodecConstraints::from_config(&runtime_config);
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 startup codec constraints: {}", e),
}
}
// Start enabled extensions // Start enabled extensions
{ {
let ext_config = config_store.get(); let ext_config = config_store.get();
@@ -886,6 +930,15 @@ async fn cleanup(state: &Arc<AppState>) {
} }
} }
// Stop RTSP service
if let Some(ref service) = *state.rtsp.read().await {
if let Err(e) = service.stop().await {
tracing::warn!("Failed to stop RTSP service: {}", e);
} else {
tracing::info!("RTSP service stopped");
}
}
// Stop video // Stop video
if let Err(e) = state.stream_manager.stop().await { if let Err(e) = state.stream_manager.stop().await {
tracing::warn!("Failed to stop streamer: {}", e); tracing::warn!("Failed to stop streamer: {}", e);

3
src/rtsp/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod service;
pub use service::{RtspService, RtspServiceStatus};

1235
src/rtsp/service.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,9 @@ use tracing::{debug, error, info, warn};
use crate::audio::AudioController; use crate::audio::AudioController;
use crate::hid::{HidController, KeyEventType, KeyboardEvent, KeyboardModifiers}; use crate::hid::{HidController, KeyEventType, KeyboardEvent, KeyboardModifiers};
use crate::video::codec_constraints::{
encoder_codec_to_id, encoder_codec_to_video_codec, video_codec_to_encoder_codec,
};
use crate::video::encoder::registry::{EncoderRegistry, VideoEncoderType}; use crate::video::encoder::registry::{EncoderRegistry, VideoEncoderType};
use crate::video::encoder::BitratePreset; use crate::video::encoder::BitratePreset;
use crate::video::stream_manager::VideoStreamManager; use crate::video::stream_manager::VideoStreamManager;
@@ -627,7 +630,7 @@ impl Connection {
// Select the best available video codec // Select the best available video codec
// Priority: H264 > H265 > VP8 > VP9 (H264/H265 leverage hardware encoding) // Priority: H264 > H265 > VP8 > VP9 (H264/H265 leverage hardware encoding)
let negotiated = self.negotiate_video_codec(); let negotiated = self.negotiate_video_codec().await;
self.negotiated_codec = Some(negotiated); self.negotiated_codec = Some(negotiated);
info!("Negotiated video codec: {:?}", negotiated); info!("Negotiated video codec: {:?}", negotiated);
@@ -641,28 +644,49 @@ impl Connection {
/// Negotiate video codec - select the best available encoder /// Negotiate video codec - select the best available encoder
/// Priority: H264 > H265 > VP8 > VP9 (H264/H265 leverage hardware encoding on embedded devices) /// Priority: H264 > H265 > VP8 > VP9 (H264/H265 leverage hardware encoding on embedded devices)
fn negotiate_video_codec(&self) -> VideoEncoderType { async fn negotiate_video_codec(&self) -> VideoEncoderType {
let registry = EncoderRegistry::global(); let registry = EncoderRegistry::global();
let constraints = self.current_codec_constraints().await;
// Check availability in priority order // Check availability in priority order
// H264 is preferred because it has the best hardware encoder support (RKMPP, VAAPI, etc.) // H264 is preferred because it has the best hardware encoder support (RKMPP, VAAPI, etc.)
// and most RustDesk clients support H264 hardware decoding // and most RustDesk clients support H264 hardware decoding
if registry.is_format_available(VideoEncoderType::H264, false) { if constraints.is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::H264)
&& registry.is_format_available(VideoEncoderType::H264, false)
{
return VideoEncoderType::H264; return VideoEncoderType::H264;
} }
if registry.is_format_available(VideoEncoderType::H265, false) { if constraints.is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::H265)
&& registry.is_format_available(VideoEncoderType::H265, false)
{
return VideoEncoderType::H265; return VideoEncoderType::H265;
} }
if registry.is_format_available(VideoEncoderType::VP8, false) { if constraints.is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::VP8)
&& registry.is_format_available(VideoEncoderType::VP8, false)
{
return VideoEncoderType::VP8; return VideoEncoderType::VP8;
} }
if registry.is_format_available(VideoEncoderType::VP9, false) { if constraints.is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::VP9)
&& registry.is_format_available(VideoEncoderType::VP9, false)
{
return VideoEncoderType::VP9; return VideoEncoderType::VP9;
} }
// Fallback to H264 (should be available via hardware or software encoder) // Fallback to preferred allowed codec
warn!("No video encoder available, defaulting to H264"); let preferred = constraints.preferred_webrtc_codec();
VideoEncoderType::H264 warn!(
"No allowed encoder available in priority order, falling back to {}",
encoder_codec_to_id(video_codec_to_encoder_codec(preferred))
);
video_codec_to_encoder_codec(preferred)
}
async fn current_codec_constraints(&self) -> crate::video::codec_constraints::StreamCodecConstraints {
if let Some(ref video_manager) = self.video_manager {
video_manager.codec_constraints().await
} else {
crate::video::codec_constraints::StreamCodecConstraints::unrestricted()
}
} }
/// Handle misc message with Arc writer /// Handle misc message with Arc writer
@@ -747,6 +771,17 @@ impl Connection {
if let Some(new_codec) = requested_codec { if let Some(new_codec) = requested_codec {
// Check if this codec is different from current and available // Check if this codec is different from current and available
if self.negotiated_codec != Some(new_codec) { if self.negotiated_codec != Some(new_codec) {
let constraints = self.current_codec_constraints().await;
if !constraints
.is_webrtc_codec_allowed(encoder_codec_to_video_codec(new_codec))
{
warn!(
"Client requested codec {:?} but it's blocked by constraints: {}",
new_codec, constraints.reason
);
return Ok(());
}
let registry = EncoderRegistry::global(); let registry = EncoderRegistry::global();
if registry.is_format_available(new_codec, false) { if registry.is_format_available(new_codec, false) {
info!( info!(
@@ -1080,12 +1115,21 @@ impl Connection {
if success { if success {
// Dynamically detect available encoders // Dynamically detect available encoders
let registry = EncoderRegistry::global(); let registry = EncoderRegistry::global();
let constraints = self.current_codec_constraints().await;
// Check which encoders are available (include software fallback) // Check which encoders are available (include software fallback)
let h264_available = registry.is_format_available(VideoEncoderType::H264, false); let h264_available = constraints
let h265_available = registry.is_format_available(VideoEncoderType::H265, false); .is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::H264)
let vp8_available = registry.is_format_available(VideoEncoderType::VP8, false); && registry.is_format_available(VideoEncoderType::H264, false);
let vp9_available = registry.is_format_available(VideoEncoderType::VP9, false); let h265_available = constraints
.is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::H265)
&& registry.is_format_available(VideoEncoderType::H265, false);
let vp8_available = constraints
.is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::VP8)
&& registry.is_format_available(VideoEncoderType::VP8, false);
let vp9_available = constraints
.is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::VP9)
&& registry.is_format_available(VideoEncoderType::VP9, false);
info!( info!(
"Server encoding capabilities: H264={}, H265={}, VP8={}, VP9={}", "Server encoding capabilities: H264={}, H265={}, VP8={}, VP9={}",

View File

@@ -13,6 +13,7 @@ use crate::extensions::ExtensionManager;
use crate::hid::HidController; use crate::hid::HidController;
use crate::msd::MsdController; use crate::msd::MsdController;
use crate::otg::OtgService; use crate::otg::OtgService;
use crate::rtsp::RtspService;
use crate::rustdesk::RustDeskService; use crate::rustdesk::RustDeskService;
use crate::video::VideoStreamManager; use crate::video::VideoStreamManager;
@@ -50,6 +51,8 @@ pub struct AppState {
pub audio: Arc<AudioController>, pub audio: Arc<AudioController>,
/// RustDesk remote access service (optional) /// RustDesk remote access service (optional)
pub rustdesk: Arc<RwLock<Option<Arc<RustDeskService>>>>, pub rustdesk: Arc<RwLock<Option<Arc<RustDeskService>>>>,
/// RTSP streaming service (optional)
pub rtsp: Arc<RwLock<Option<Arc<RtspService>>>>,
/// Extension manager (ttyd, gostc, easytier) /// Extension manager (ttyd, gostc, easytier)
pub extensions: Arc<ExtensionManager>, pub extensions: Arc<ExtensionManager>,
/// Event bus for real-time notifications /// Event bus for real-time notifications
@@ -76,6 +79,7 @@ impl AppState {
atx: Option<AtxController>, atx: Option<AtxController>,
audio: Arc<AudioController>, audio: Arc<AudioController>,
rustdesk: Option<Arc<RustDeskService>>, rustdesk: Option<Arc<RustDeskService>>,
rtsp: Option<Arc<RtspService>>,
extensions: Arc<ExtensionManager>, extensions: Arc<ExtensionManager>,
events: Arc<EventBus>, events: Arc<EventBus>,
shutdown_tx: broadcast::Sender<()>, shutdown_tx: broadcast::Sender<()>,
@@ -92,6 +96,7 @@ impl AppState {
atx: Arc::new(RwLock::new(atx)), atx: Arc::new(RwLock::new(atx)),
audio, audio,
rustdesk: Arc::new(RwLock::new(rustdesk)), rustdesk: Arc::new(RwLock::new(rustdesk)),
rtsp: Arc::new(RwLock::new(rtsp)),
extensions, extensions,
events, events,
shutdown_tx, shutdown_tx,

View File

@@ -0,0 +1,193 @@
use crate::config::{AppConfig, RtspCodec, StreamMode};
use crate::error::Result;
use crate::video::encoder::registry::VideoEncoderType;
use crate::video::encoder::VideoCodecType;
use crate::video::VideoStreamManager;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct StreamCodecConstraints {
pub rustdesk_enabled: bool,
pub rtsp_enabled: bool,
pub allowed_webrtc_codecs: Vec<VideoCodecType>,
pub allow_mjpeg: bool,
pub locked_codec: Option<VideoCodecType>,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct ConstraintEnforcementResult {
pub changed: bool,
pub message: Option<String>,
}
impl StreamCodecConstraints {
pub fn unrestricted() -> Self {
Self {
rustdesk_enabled: false,
rtsp_enabled: false,
allowed_webrtc_codecs: vec![
VideoCodecType::H264,
VideoCodecType::H265,
VideoCodecType::VP8,
VideoCodecType::VP9,
],
allow_mjpeg: true,
locked_codec: None,
reason: "No codec lock active".to_string(),
}
}
pub fn from_config(config: &AppConfig) -> Self {
let rustdesk_enabled = config.rustdesk.enabled;
let rtsp_enabled = config.rtsp.enabled;
if rtsp_enabled {
let locked_codec = match config.rtsp.codec {
RtspCodec::H264 => VideoCodecType::H264,
RtspCodec::H265 => VideoCodecType::H265,
};
return Self {
rustdesk_enabled,
rtsp_enabled,
allowed_webrtc_codecs: vec![locked_codec],
allow_mjpeg: false,
locked_codec: Some(locked_codec),
reason: if rustdesk_enabled {
format!(
"RTSP enabled with codec lock ({:?}) and RustDesk enabled",
locked_codec
)
} else {
format!("RTSP enabled with codec lock ({:?})", locked_codec)
},
};
}
if rustdesk_enabled {
return Self {
rustdesk_enabled,
rtsp_enabled,
allowed_webrtc_codecs: vec![
VideoCodecType::H264,
VideoCodecType::H265,
VideoCodecType::VP8,
VideoCodecType::VP9,
],
allow_mjpeg: false,
locked_codec: None,
reason: "RustDesk enabled, MJPEG disabled".to_string(),
};
}
Self::unrestricted()
}
pub fn is_mjpeg_allowed(&self) -> bool {
self.allow_mjpeg
}
pub fn is_webrtc_codec_allowed(&self, codec: VideoCodecType) -> bool {
self.allowed_webrtc_codecs.contains(&codec)
}
pub fn preferred_webrtc_codec(&self) -> VideoCodecType {
if let Some(codec) = self.locked_codec {
return codec;
}
self.allowed_webrtc_codecs
.first()
.copied()
.unwrap_or(VideoCodecType::H264)
}
pub fn allowed_codecs_for_api(&self) -> Vec<&'static str> {
let mut codecs = Vec::new();
if self.allow_mjpeg {
codecs.push("mjpeg");
}
for codec in &self.allowed_webrtc_codecs {
codecs.push(codec_to_id(*codec));
}
codecs
}
}
pub async fn enforce_constraints_with_stream_manager(
stream_manager: &Arc<VideoStreamManager>,
constraints: &StreamCodecConstraints,
) -> Result<ConstraintEnforcementResult> {
let current_mode = stream_manager.current_mode().await;
if current_mode == StreamMode::Mjpeg && !constraints.allow_mjpeg {
let target_codec = constraints.preferred_webrtc_codec();
stream_manager.set_video_codec(target_codec).await?;
let _ = stream_manager
.switch_mode_transaction(StreamMode::WebRTC)
.await?;
return Ok(ConstraintEnforcementResult {
changed: true,
message: Some(format!(
"Auto-switched from MJPEG to {} due to codec lock",
codec_to_id(target_codec)
)),
});
}
if current_mode == StreamMode::WebRTC {
let current_codec = stream_manager.webrtc_streamer().current_video_codec().await;
if !constraints.is_webrtc_codec_allowed(current_codec) {
let target_codec = constraints.preferred_webrtc_codec();
stream_manager.set_video_codec(target_codec).await?;
return Ok(ConstraintEnforcementResult {
changed: true,
message: Some(format!(
"Auto-switched codec from {} to {} due to codec lock",
codec_to_id(current_codec),
codec_to_id(target_codec)
)),
});
}
}
Ok(ConstraintEnforcementResult {
changed: false,
message: None,
})
}
pub fn codec_to_id(codec: VideoCodecType) -> &'static str {
match codec {
VideoCodecType::H264 => "h264",
VideoCodecType::H265 => "h265",
VideoCodecType::VP8 => "vp8",
VideoCodecType::VP9 => "vp9",
}
}
pub fn encoder_codec_to_id(codec: VideoEncoderType) -> &'static str {
match codec {
VideoEncoderType::H264 => "h264",
VideoEncoderType::H265 => "h265",
VideoEncoderType::VP8 => "vp8",
VideoEncoderType::VP9 => "vp9",
}
}
pub fn video_codec_to_encoder_codec(codec: VideoCodecType) -> VideoEncoderType {
match codec {
VideoCodecType::H264 => VideoEncoderType::H264,
VideoCodecType::H265 => VideoEncoderType::H265,
VideoCodecType::VP8 => VideoEncoderType::VP8,
VideoCodecType::VP9 => VideoEncoderType::VP9,
}
}
pub fn encoder_codec_to_video_codec(codec: VideoEncoderType) -> VideoCodecType {
match codec {
VideoEncoderType::H264 => VideoCodecType::H264,
VideoEncoderType::H265 => VideoCodecType::H265,
VideoEncoderType::VP8 => VideoCodecType::VP8,
VideoEncoderType::VP9 => VideoCodecType::VP9,
}
}

View File

@@ -3,6 +3,7 @@
//! This module provides V4L2 video capture, encoding, and streaming functionality. //! This module provides V4L2 video capture, encoding, and streaming functionality.
pub mod capture; pub mod capture;
pub mod codec_constraints;
pub mod convert; pub mod convert;
pub mod decoder; pub mod decoder;
pub mod device; pub mod device;

View File

@@ -37,6 +37,7 @@ use crate::error::Result;
use crate::events::{EventBus, SystemEvent, VideoDeviceInfo}; use crate::events::{EventBus, SystemEvent, VideoDeviceInfo};
use crate::hid::HidController; use crate::hid::HidController;
use crate::stream::MjpegStreamHandler; use crate::stream::MjpegStreamHandler;
use crate::video::codec_constraints::StreamCodecConstraints;
use crate::video::format::{PixelFormat, Resolution}; use crate::video::format::{PixelFormat, Resolution};
use crate::video::streamer::{Streamer, StreamerState}; use crate::video::streamer::{Streamer, StreamerState};
use crate::webrtc::WebRtcStreamer; use crate::webrtc::WebRtcStreamer;
@@ -144,6 +145,16 @@ impl VideoStreamManager {
*self.config_store.write().await = Some(config); *self.config_store.write().await = Some(config);
} }
/// Get current stream codec constraints derived from global configuration.
pub async fn codec_constraints(&self) -> StreamCodecConstraints {
if let Some(ref config_store) = *self.config_store.read().await {
let config = config_store.get();
StreamCodecConstraints::from_config(&config)
} else {
StreamCodecConstraints::unrestricted()
}
}
/// Get current streaming mode /// Get current streaming mode
pub async fn current_mode(&self) -> StreamMode { pub async fn current_mode(&self) -> StreamMode {
self.mode.read().await.clone() self.mode.read().await.clone()

View File

@@ -7,7 +7,11 @@ use std::sync::Arc;
use crate::config::*; use crate::config::*;
use crate::error::{AppError, Result}; use crate::error::{AppError, Result};
use crate::events::SystemEvent; use crate::events::SystemEvent;
use crate::rtsp::RtspService;
use crate::state::AppState; use crate::state::AppState;
use crate::video::codec_constraints::{
enforce_constraints_with_stream_manager, StreamCodecConstraints,
};
/// 应用 Video 配置变更 /// 应用 Video 配置变更
pub async fn apply_video_config( pub async fn apply_video_config(
@@ -444,6 +448,15 @@ pub async fn apply_audio_config(
Ok(()) Ok(())
} }
/// Apply stream codec constraints derived from global config.
pub async fn enforce_stream_codec_constraints(state: &Arc<AppState>) -> Result<Option<String>> {
let config = state.config.get();
let constraints = StreamCodecConstraints::from_config(&config);
let enforcement =
enforce_constraints_with_stream_manager(&state.stream_manager, &constraints).await?;
Ok(enforcement.message)
}
/// 应用 RustDesk 配置变更 /// 应用 RustDesk 配置变更
pub async fn apply_rustdesk_config( pub async fn apply_rustdesk_config(
state: &Arc<AppState>, state: &Arc<AppState>,
@@ -453,6 +466,7 @@ pub async fn apply_rustdesk_config(
tracing::info!("Applying RustDesk config changes..."); tracing::info!("Applying RustDesk config changes...");
let mut rustdesk_guard = state.rustdesk.write().await; let mut rustdesk_guard = state.rustdesk.write().await;
let mut credentials_to_save = None;
// Check if service needs to be stopped // Check if service needs to be stopped
if old_config.enabled && !new_config.enabled { if old_config.enabled && !new_config.enabled {
@@ -464,7 +478,6 @@ pub async fn apply_rustdesk_config(
tracing::info!("RustDesk service stopped"); tracing::info!("RustDesk service stopped");
} }
*rustdesk_guard = None; *rustdesk_guard = None;
return Ok(());
} }
// Check if service needs to be started or restarted // Check if service needs to be started or restarted
@@ -473,8 +486,6 @@ pub async fn apply_rustdesk_config(
|| old_config.device_id != new_config.device_id || old_config.device_id != new_config.device_id
|| old_config.device_password != new_config.device_password; || old_config.device_password != new_config.device_password;
let mut credentials_to_save = None;
if rustdesk_guard.is_none() { if rustdesk_guard.is_none() {
// Create new service // Create new service
tracing::info!("Initializing RustDesk service..."); tracing::info!("Initializing RustDesk service...");
@@ -507,6 +518,7 @@ pub async fn apply_rustdesk_config(
} }
} }
} }
}
// Save credentials to persistent config store (outside the lock) // Save credentials to persistent config store (outside the lock)
drop(rustdesk_guard); drop(rustdesk_guard);
@@ -528,6 +540,59 @@ pub async fn apply_rustdesk_config(
tracing::info!("RustDesk credentials saved successfully"); tracing::info!("RustDesk credentials saved successfully");
} }
} }
if let Some(message) = enforce_stream_codec_constraints(state).await? {
tracing::info!("{}", message);
}
Ok(())
}
/// 应用 RTSP 配置变更
pub async fn apply_rtsp_config(
state: &Arc<AppState>,
old_config: &RtspConfig,
new_config: &RtspConfig,
) -> Result<()> {
tracing::info!("Applying RTSP config changes...");
let mut rtsp_guard = state.rtsp.write().await;
if old_config.enabled && !new_config.enabled {
if let Some(ref service) = *rtsp_guard {
if let Err(e) = service.stop().await {
tracing::error!("Failed to stop RTSP service: {}", e);
}
}
*rtsp_guard = None;
}
if new_config.enabled {
let need_restart = 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 rtsp_guard.is_none() {
let service = RtspService::new(new_config.clone(), state.stream_manager.clone());
service.start().await?;
tracing::info!("RTSP service started");
*rtsp_guard = Some(Arc::new(service));
} else if need_restart {
if let Some(ref service) = *rtsp_guard {
service.restart(new_config.clone()).await?;
tracing::info!("RTSP service restarted");
}
}
}
drop(rtsp_guard);
if let Some(message) = enforce_stream_codec_constraints(state).await? {
tracing::info!("{}", message);
} }
Ok(()) Ok(())

View File

@@ -25,6 +25,7 @@ mod auth;
mod hid; mod hid;
mod msd; mod msd;
mod rustdesk; mod rustdesk;
mod rtsp;
mod stream; mod stream;
pub(crate) mod video; pub(crate) mod video;
mod web; mod web;
@@ -39,6 +40,7 @@ pub use rustdesk::{
get_device_password, get_rustdesk_config, get_rustdesk_status, regenerate_device_id, get_device_password, get_rustdesk_config, get_rustdesk_status, regenerate_device_id,
regenerate_device_password, update_rustdesk_config, regenerate_device_password, update_rustdesk_config,
}; };
pub use rtsp::{get_rtsp_config, get_rtsp_status, update_rtsp_config};
pub use stream::{get_stream_config, update_stream_config}; pub use stream::{get_stream_config, update_stream_config};
pub use video::{get_video_config, update_video_config}; pub use video::{get_video_config, update_video_config};
pub use web::{get_web_config, update_web_config}; pub use web::{get_web_config, update_web_config};
@@ -64,6 +66,9 @@ fn sanitize_config_for_api(config: &mut AppConfig) {
config.rustdesk.private_key = None; config.rustdesk.private_key = None;
config.rustdesk.signing_public_key = None; config.rustdesk.signing_public_key = None;
config.rustdesk.signing_private_key = None; config.rustdesk.signing_private_key = None;
// RTSP secrets
config.rtsp.password = None;
} }
/// 获取完整配置 /// 获取完整配置

View File

@@ -0,0 +1,67 @@
use axum::{extract::State, Json};
use std::sync::Arc;
use crate::error::{AppError, Result};
use crate::state::AppState;
use super::apply::apply_rtsp_config;
use super::types::{RtspConfigResponse, RtspConfigUpdate, RtspStatusResponse};
/// Get RTSP config
pub async fn get_rtsp_config(State(state): State<Arc<AppState>>) -> Json<RtspConfigResponse> {
let config = state.config.get();
Json(RtspConfigResponse::from(&config.rtsp))
}
/// Get RTSP status (config + service status)
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
}
};
Json(RtspStatusResponse::new(&config, status))
}
/// Update RTSP config
pub async fn update_rtsp_config(
State(state): State<Arc<AppState>>,
Json(req): Json<RtspConfigUpdate>,
) -> Result<Json<RtspConfigResponse>> {
req.validate()?;
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();
if let Err(err) = apply_rtsp_config(&state, &old_config, &new_config).await {
tracing::error!("Failed to apply RTSP config: {}", err);
if let Err(rollback_err) = state
.config
.update(|config| {
config.rtsp = old_config.clone();
})
.await
{
tracing::error!("Failed to rollback RTSP config after apply failure: {}", rollback_err);
return Err(AppError::ServiceUnavailable(format!(
"RTSP apply failed: {}; rollback failed: {}",
err, rollback_err
)));
}
return Err(err);
}
Ok(Json(RtspConfigResponse::from(&new_config)))
}

View File

@@ -106,6 +106,15 @@ pub async fn update_rustdesk_config(
tracing::error!("Failed to apply RustDesk config: {}", e); tracing::error!("Failed to apply RustDesk config: {}", e);
} }
// Share a non-sensitive summary for frontend UX
let constraints = state.stream_manager.codec_constraints().await;
if constraints.rustdesk_enabled || constraints.rtsp_enabled {
tracing::info!(
"Stream codec constraints active after RustDesk update: {}",
constraints.reason
);
}
Ok(Json(RustDeskConfigResponse::from(&new_config))) Ok(Json(RustDeskConfigResponse::from(&new_config)))
} }

View File

@@ -42,5 +42,10 @@ pub async fn update_stream_config(
tracing::error!("Failed to apply stream config: {}", e); tracing::error!("Failed to apply stream config: {}", e);
} }
// 6. Enforce codec constraints after any stream config update
if let Err(e) = super::apply::enforce_stream_codec_constraints(&state).await {
tracing::error!("Failed to enforce stream codec constraints: {}", e);
}
Ok(Json(StreamConfigResponse::from(&new_stream_config))) Ok(Json(StreamConfigResponse::from(&new_stream_config)))
} }

View File

@@ -1,5 +1,6 @@
use crate::config::*; use crate::config::*;
use crate::error::AppError; use crate::error::AppError;
use crate::rtsp::RtspServiceStatus;
use crate::rustdesk::config::RustDeskConfig; use crate::rustdesk::config::RustDeskConfig;
use crate::video::encoder::BitratePreset; use crate::video::encoder::BitratePreset;
use serde::Deserialize; use serde::Deserialize;
@@ -604,6 +605,124 @@ impl RustDeskConfigUpdate {
} }
} }
// ===== RTSP Config =====
#[typeshare]
#[derive(Debug, serde::Serialize)]
pub struct RtspConfigResponse {
pub enabled: bool,
pub bind: String,
pub port: u16,
pub path: String,
pub allow_one_client: bool,
pub codec: RtspCodec,
pub username: Option<String>,
pub has_password: bool,
}
impl From<&RtspConfig> for RtspConfigResponse {
fn from(config: &RtspConfig) -> Self {
Self {
enabled: config.enabled,
bind: config.bind.clone(),
port: config.port,
path: config.path.clone(),
allow_one_client: config.allow_one_client,
codec: config.codec.clone(),
username: config.username.clone(),
has_password: config.password.is_some(),
}
}
}
#[typeshare]
#[derive(Debug, serde::Serialize)]
pub struct RtspStatusResponse {
pub config: RtspConfigResponse,
pub service_status: String,
}
impl RtspStatusResponse {
pub fn new(config: &RtspConfig, status: RtspServiceStatus) -> Self {
Self {
config: RtspConfigResponse::from(config),
service_status: status.to_string(),
}
}
}
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct RtspConfigUpdate {
pub enabled: Option<bool>,
pub bind: Option<String>,
pub port: Option<u16>,
pub path: Option<String>,
pub allow_one_client: Option<bool>,
pub codec: Option<RtspCodec>,
pub username: Option<String>,
pub password: Option<String>,
}
impl RtspConfigUpdate {
pub fn validate(&self) -> crate::error::Result<()> {
if let Some(port) = self.port {
if port == 0 {
return Err(AppError::BadRequest("RTSP port cannot be 0".into()));
}
}
if let Some(ref bind) = self.bind {
if bind.parse::<std::net::IpAddr>().is_err() {
return Err(AppError::BadRequest("RTSP bind must be a valid IP".into()));
}
}
if let Some(ref path) = self.path {
let normalized = path.trim_matches('/');
if normalized.is_empty() {
return Err(AppError::BadRequest("RTSP path cannot be empty".into()));
}
}
Ok(())
}
pub fn apply_to(&self, config: &mut RtspConfig) {
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 path) = self.path {
config.path = path.trim_matches('/').to_string();
}
if let Some(allow_one_client) = self.allow_one_client {
config.allow_one_client = allow_one_client;
}
if let Some(codec) = self.codec.clone() {
config.codec = codec;
}
if let Some(ref username) = self.username {
config.username = if username.is_empty() {
None
} else {
Some(username.clone())
};
}
if let Some(ref password) = self.password {
config.password = if password.is_empty() {
None
} else {
Some(password.clone())
};
}
}
}
// ===== Web Config ===== // ===== Web Config =====
#[typeshare] #[typeshare]
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]

View File

@@ -14,6 +14,7 @@ use crate::config::{AppConfig, StreamMode};
use crate::error::{AppError, Result}; use crate::error::{AppError, Result};
use crate::events::SystemEvent; use crate::events::SystemEvent;
use crate::state::AppState; use crate::state::AppState;
use crate::video::codec_constraints::codec_to_id;
use crate::video::encoder::BitratePreset; use crate::video::encoder::BitratePreset;
// ============================================================================ // ============================================================================
@@ -747,6 +748,17 @@ pub async fn setup_init(
} }
} }
// Start RTSP if enabled
if new_config.rtsp.enabled {
let empty_config = crate::config::RtspConfig::default();
if let Err(e) = config::apply::apply_rtsp_config(&state, &empty_config, &new_config.rtsp).await
{
tracing::warn!("Failed to start RTSP during setup: {}", e);
} else {
tracing::info!("RTSP started during setup");
}
}
// Start audio streaming if audio device was selected during setup // Start audio streaming if audio device was selected during setup
if new_config.audio.enabled { if new_config.audio.enabled {
let audio_config = crate::audio::AudioControllerConfig { let audio_config = crate::audio::AudioControllerConfig {
@@ -1439,6 +1451,8 @@ pub async fn stream_mode_set(
) -> Result<Json<StreamModeResponse>> { ) -> Result<Json<StreamModeResponse>> {
use crate::video::encoder::VideoCodecType; use crate::video::encoder::VideoCodecType;
let constraints = state.stream_manager.codec_constraints().await;
let mode_lower = req.mode.to_lowercase(); let mode_lower = req.mode.to_lowercase();
let (new_mode, video_codec) = match mode_lower.as_str() { let (new_mode, video_codec) = match mode_lower.as_str() {
"mjpeg" => (StreamMode::Mjpeg, None), "mjpeg" => (StreamMode::Mjpeg, None),
@@ -1454,6 +1468,23 @@ pub async fn stream_mode_set(
} }
}; };
if new_mode == StreamMode::Mjpeg && !constraints.is_mjpeg_allowed() {
return Err(AppError::BadRequest(format!(
"Codec 'mjpeg' is not allowed: {}",
constraints.reason
)));
}
if let Some(codec) = video_codec {
if !constraints.is_webrtc_codec_allowed(codec) {
return Err(AppError::BadRequest(format!(
"Codec '{}' is not allowed: {}",
codec_to_id(codec),
constraints.reason
)));
}
}
// Set video codec if switching to WebRTC mode with specific codec // Set video codec if switching to WebRTC mode with specific codec
if let Some(codec) = video_codec { if let Some(codec) = video_codec {
info!("Setting WebRTC video codec to {:?}", codec); info!("Setting WebRTC video codec to {:?}", codec);
@@ -1560,6 +1591,67 @@ pub struct AvailableCodecsResponse {
pub codecs: Vec<VideoCodecInfo>, pub codecs: Vec<VideoCodecInfo>,
} }
/// Stream constraints response
#[derive(Serialize)]
pub struct StreamConstraintsResponse {
pub success: bool,
pub allowed_codecs: Vec<String>,
pub locked_codec: Option<String>,
pub disallow_mjpeg: bool,
pub sources: ConstraintSources,
pub reason: String,
pub current_mode: String,
}
#[derive(Serialize)]
pub struct ConstraintSources {
pub rustdesk: bool,
pub rtsp: bool,
}
/// Get stream codec constraints derived from enabled services.
pub async fn stream_constraints_get(
State(state): State<Arc<AppState>>,
) -> Json<StreamConstraintsResponse> {
use crate::video::encoder::VideoCodecType;
let constraints = state.stream_manager.codec_constraints().await;
let current_mode = state.stream_manager.current_mode().await;
let current_mode = match current_mode {
StreamMode::Mjpeg => "mjpeg".to_string(),
StreamMode::WebRTC => {
let codec = state
.stream_manager
.webrtc_streamer()
.current_video_codec()
.await;
match codec {
VideoCodecType::H264 => "h264".to_string(),
VideoCodecType::H265 => "h265".to_string(),
VideoCodecType::VP8 => "vp8".to_string(),
VideoCodecType::VP9 => "vp9".to_string(),
}
}
};
Json(StreamConstraintsResponse {
success: true,
allowed_codecs: constraints
.allowed_codecs_for_api()
.into_iter()
.map(str::to_string)
.collect(),
locked_codec: constraints.locked_codec.map(codec_to_id).map(str::to_string),
disallow_mjpeg: !constraints.allow_mjpeg,
sources: ConstraintSources {
rustdesk: constraints.rustdesk_enabled,
rtsp: constraints.rtsp_enabled,
},
reason: constraints.reason,
current_mode,
})
}
/// Set bitrate request /// Set bitrate request
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct SetBitrateRequest { pub struct SetBitrateRequest {

View File

@@ -50,6 +50,7 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.route("/stream/mode", post(handlers::stream_mode_set)) .route("/stream/mode", post(handlers::stream_mode_set))
.route("/stream/bitrate", post(handlers::stream_set_bitrate)) .route("/stream/bitrate", post(handlers::stream_set_bitrate))
.route("/stream/codecs", get(handlers::stream_codecs_list)) .route("/stream/codecs", get(handlers::stream_codecs_list))
.route("/stream/constraints", get(handlers::stream_constraints_get))
// WebRTC endpoints // WebRTC endpoints
.route("/webrtc/session", post(handlers::webrtc_create_session)) .route("/webrtc/session", post(handlers::webrtc_create_session))
.route("/webrtc/offer", post(handlers::webrtc_offer)) .route("/webrtc/offer", post(handlers::webrtc_offer))
@@ -120,6 +121,13 @@ pub fn create_router(state: Arc<AppState>) -> Router {
"/config/rustdesk/regenerate-password", "/config/rustdesk/regenerate-password",
post(handlers::config::regenerate_device_password), post(handlers::config::regenerate_device_password),
) )
// RTSP configuration endpoints
.route("/config/rtsp", get(handlers::config::get_rtsp_config))
.route("/config/rtsp", patch(handlers::config::update_rtsp_config))
.route(
"/config/rtsp/status",
get(handlers::config::get_rtsp_status),
)
// Web server configuration // Web server configuration
.route("/config/web", get(handlers::config::get_web_config)) .route("/config/web", get(handlers::config::get_web_config))
.route("/config/web", patch(handlers::config::update_web_config)) .route("/config/web", patch(handlers::config::update_web_config))

View File

@@ -330,6 +330,49 @@ export const rustdeskConfigApi = {
}), }),
} }
// ===== RTSP 配置 API =====
export type RtspCodec = 'h264' | 'h265'
export interface RtspConfigResponse {
enabled: boolean
bind: string
port: number
path: string
allow_one_client: boolean
codec: RtspCodec
username?: string | null
has_password: boolean
}
export interface RtspConfigUpdate {
enabled?: boolean
bind?: string
port?: number
path?: string
allow_one_client?: boolean
codec?: RtspCodec
username?: string
password?: string
}
export interface RtspStatusResponse {
config: RtspConfigResponse
service_status: string
}
export const rtspConfigApi = {
get: () => request<RtspConfigResponse>('/config/rtsp'),
update: (config: RtspConfigUpdate) =>
request<RtspConfigResponse>('/config/rtsp', {
method: 'PATCH',
body: JSON.stringify(config),
}),
getStatus: () => request<RtspStatusResponse>('/config/rtsp/status'),
}
// ===== Web 服务器配置 API ===== // ===== Web 服务器配置 API =====
/** Web 服务器配置 */ /** Web 服务器配置 */

View File

@@ -124,6 +124,19 @@ export interface AvailableCodecsResponse {
codecs: VideoCodecInfo[] codecs: VideoCodecInfo[]
} }
export interface StreamConstraintsResponse {
success: boolean
allowed_codecs: string[]
locked_codec: string | null
disallow_mjpeg: boolean
sources: {
rustdesk: boolean
rtsp: boolean
}
reason: string
current_mode: string
}
export const streamApi = { export const streamApi = {
status: () => status: () =>
request<{ request<{
@@ -161,6 +174,9 @@ export const streamApi = {
getCodecs: () => getCodecs: () =>
request<AvailableCodecsResponse>('/stream/codecs'), request<AvailableCodecsResponse>('/stream/codecs'),
getConstraints: () =>
request<StreamConstraintsResponse>('/stream/constraints'),
setBitratePreset: (bitrate_preset: import('@/types/generated').BitratePreset) => setBitratePreset: (bitrate_preset: import('@/types/generated').BitratePreset) =>
request<{ success: boolean; message?: string }>('/stream/bitrate', { request<{ success: boolean; message?: string }>('/stream/bitrate', {
method: 'POST', method: 'POST',
@@ -536,11 +552,15 @@ export {
audioConfigApi, audioConfigApi,
extensionsApi, extensionsApi,
rustdeskConfigApi, rustdeskConfigApi,
rtspConfigApi,
webConfigApi, webConfigApi,
type RustDeskConfigResponse, type RustDeskConfigResponse,
type RustDeskStatusResponse, type RustDeskStatusResponse,
type RustDeskConfigUpdate, type RustDeskConfigUpdate,
type RustDeskPasswordResponse, type RustDeskPasswordResponse,
type RtspConfigResponse,
type RtspConfigUpdate,
type RtspStatusResponse,
type WebConfig, type WebConfig,
} from './config' } from './config'

View File

@@ -19,7 +19,14 @@ import {
} from '@/components/ui/select' } from '@/components/ui/select'
import { Monitor, RefreshCw, Loader2, Settings, Zap, Scale, Image } from 'lucide-vue-next' import { Monitor, RefreshCw, Loader2, Settings, Zap, Scale, Image } from 'lucide-vue-next'
import HelpTooltip from '@/components/HelpTooltip.vue' import HelpTooltip from '@/components/HelpTooltip.vue'
import { configApi, streamApi, type VideoCodecInfo, type EncoderBackendInfo, type BitratePreset } from '@/api' import {
configApi,
streamApi,
type VideoCodecInfo,
type EncoderBackendInfo,
type BitratePreset,
type StreamConstraintsResponse,
} from '@/api'
import { useConfigStore } from '@/stores/config' import { useConfigStore } from '@/stores/config'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -64,6 +71,7 @@ const loadingCodecs = ref(false)
// Backend list // Backend list
const backends = ref<EncoderBackendInfo[]>([]) const backends = ref<EncoderBackendInfo[]>([])
const constraints = ref<StreamConstraintsResponse | null>(null)
const currentEncoderBackend = computed(() => configStore.stream?.encoder || 'auto') const currentEncoderBackend = computed(() => configStore.stream?.encoder || 'auto')
// Browser supported codecs (WebRTC receive capabilities) // Browser supported codecs (WebRTC receive capabilities)
@@ -220,7 +228,7 @@ const availableCodecs = computed(() => {
const backend = backends.value.find(b => b.id === currentEncoderBackend.value) const backend = backends.value.find(b => b.id === currentEncoderBackend.value)
if (!backend) return allAvailable if (!backend) return allAvailable
return allAvailable const backendFiltered = allAvailable
.filter(codec => { .filter(codec => {
// MJPEG is always available (doesn't require encoder) // MJPEG is always available (doesn't require encoder)
if (codec.id === 'mjpeg') return true if (codec.id === 'mjpeg') return true
@@ -238,6 +246,13 @@ const availableCodecs = computed(() => {
backend: backend.name, backend: backend.name,
} }
}) })
const allowed = constraints.value?.allowed_codecs
if (!allowed || allowed.length === 0) {
return backendFiltered
}
return backendFiltered.filter(codec => allowed.includes(codec.id))
}) })
// Cascading filters // Cascading filters
@@ -303,6 +318,14 @@ async function loadCodecs() {
} }
} }
async function loadConstraints() {
try {
constraints.value = await streamApi.getConstraints()
} catch {
constraints.value = null
}
}
// Navigate to settings page (video tab) // Navigate to settings page (video tab)
function goToSettings() { function goToSettings() {
router.push('/settings?tab=video') router.push('/settings?tab=video')
@@ -339,6 +362,12 @@ function syncFromCurrentIfChanged() {
// Handle video mode change // Handle video mode change
function handleVideoModeChange(mode: unknown) { function handleVideoModeChange(mode: unknown) {
if (typeof mode !== 'string') return if (typeof mode !== 'string') return
if (constraints.value?.allowed_codecs?.length && !constraints.value.allowed_codecs.includes(mode)) {
toast.error(constraints.value.reason || t('actionbar.selectMode'))
return
}
emit('update:videoMode', mode as VideoMode) emit('update:videoMode', mode as VideoMode)
} }
@@ -466,6 +495,8 @@ watch(() => props.open, (isOpen) => {
loadCodecs() loadCodecs()
} }
loadConstraints()
Promise.all([ Promise.all([
configStore.refreshVideo(), configStore.refreshVideo(),
configStore.refreshStream(), configStore.refreshStream(),

View File

@@ -357,6 +357,30 @@ export interface RustDeskConfig {
device_id: string; device_id: string;
} }
/** RTSP output codec */
export enum RtspCodec {
H264 = "h264",
H265 = "h265",
}
/** RTSP configuration */
export interface RtspConfig {
/** Enable RTSP output */
enabled: boolean;
/** Bind IP address */
bind: string;
/** RTSP TCP listen port */
port: number;
/** Stream path (without leading slash) */
path: string;
/** Allow only one client connection at a time */
allow_one_client: boolean;
/** Output codec (H264/H265) */
codec: RtspCodec;
/** Optional username for authentication */
username?: string;
}
/** Main application configuration */ /** Main application configuration */
export interface AppConfig { export interface AppConfig {
/** Whether initial setup has been completed */ /** Whether initial setup has been completed */
@@ -381,6 +405,8 @@ export interface AppConfig {
extensions: ExtensionsConfig; extensions: ExtensionsConfig;
/** RustDesk remote access settings */ /** RustDesk remote access settings */
rustdesk: RustDeskConfig; rustdesk: RustDeskConfig;
/** RTSP streaming settings */
rtsp: RtspConfig;
} }
/** Update for a single ATX key configuration */ /** Update for a single ATX key configuration */
@@ -557,6 +583,33 @@ export interface MsdConfigUpdate {
msd_dir?: string; msd_dir?: string;
} }
export interface RtspConfigResponse {
enabled: boolean;
bind: string;
port: number;
path: string;
allow_one_client: boolean;
codec: RtspCodec;
username?: string;
has_password: boolean;
}
export interface RtspConfigUpdate {
enabled?: boolean;
bind?: string;
port?: number;
path?: string;
allow_one_client?: boolean;
codec?: RtspCodec;
username?: string;
password?: string;
}
export interface RtspStatusResponse {
config: RtspConfigResponse;
service_status: string;
}
export interface RustDeskConfigUpdate { export interface RustDeskConfigUpdate {
enabled?: boolean; enabled?: boolean;
rendezvous_server?: string; rendezvous_server?: string;