mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-15 07:26:44 +08:00
feat: 支持 rtsp 功能
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
53
src/main.rs
53
src/main.rs
@@ -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
3
src/rtsp/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod service;
|
||||||
|
|
||||||
|
pub use service::{RtspService, RtspServiceStatus};
|
||||||
1235
src/rtsp/service.rs
Normal file
1235
src/rtsp/service.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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={}",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
193
src/video/codec_constraints.rs
Normal file
193
src/video/codec_constraints.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取完整配置
|
/// 获取完整配置
|
||||||
|
|||||||
67
src/web/handlers/config/rtsp.rs
Normal file
67
src/web/handlers/config/rtsp.rs
Normal 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)))
|
||||||
|
}
|
||||||
@@ -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)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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 服务器配置 */
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user