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 = "0.14"
|
||||
rtp = "0.14"
|
||||
rtsp-types = "0.1"
|
||||
sdp-types = "0.1"
|
||||
|
||||
# Audio (ALSA capture + Opus encoding)
|
||||
# Note: audiopus links to libopus.so (unavoidable for audio support)
|
||||
|
||||
@@ -35,6 +35,8 @@ pub struct AppConfig {
|
||||
pub extensions: ExtensionsConfig,
|
||||
/// RustDesk remote access settings
|
||||
pub rustdesk: RustDeskConfig,
|
||||
/// RTSP streaming settings
|
||||
pub rtsp: RtspConfig,
|
||||
}
|
||||
|
||||
|
||||
@@ -404,6 +406,56 @@ pub enum StreamMode {
|
||||
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
|
||||
#[typeshare]
|
||||
|
||||
@@ -14,6 +14,7 @@ pub mod hid;
|
||||
pub mod modules;
|
||||
pub mod msd;
|
||||
pub mod otg;
|
||||
pub mod rtsp;
|
||||
pub mod rustdesk;
|
||||
pub mod state;
|
||||
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::msd::MsdController;
|
||||
use one_kvm::otg::{configfs, OtgService};
|
||||
use one_kvm::rtsp::RtspService;
|
||||
use one_kvm::rustdesk::RustDeskService;
|
||||
use one_kvm::state::AppState;
|
||||
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::{Streamer, VideoStreamManager};
|
||||
use one_kvm::web;
|
||||
@@ -534,6 +538,21 @@ async fn main() -> anyhow::Result<()> {
|
||||
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
|
||||
let state = AppState::new(
|
||||
config_store.clone(),
|
||||
@@ -546,6 +565,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
atx,
|
||||
audio,
|
||||
rustdesk.clone(),
|
||||
rtsp.clone(),
|
||||
extensions.clone(),
|
||||
events.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
|
||||
{
|
||||
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
|
||||
if let Err(e) = state.stream_manager.stop().await {
|
||||
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::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::BitratePreset;
|
||||
use crate::video::stream_manager::VideoStreamManager;
|
||||
@@ -627,7 +630,7 @@ impl Connection {
|
||||
|
||||
// Select the best available video codec
|
||||
// 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);
|
||||
info!("Negotiated video codec: {:?}", negotiated);
|
||||
|
||||
@@ -641,28 +644,49 @@ impl Connection {
|
||||
|
||||
/// Negotiate video codec - select the best available encoder
|
||||
/// 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 constraints = self.current_codec_constraints().await;
|
||||
|
||||
// Check availability in priority order
|
||||
// H264 is preferred because it has the best hardware encoder support (RKMPP, VAAPI, etc.)
|
||||
// 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// Fallback to H264 (should be available via hardware or software encoder)
|
||||
warn!("No video encoder available, defaulting to H264");
|
||||
VideoEncoderType::H264
|
||||
// Fallback to preferred allowed codec
|
||||
let preferred = constraints.preferred_webrtc_codec();
|
||||
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
|
||||
@@ -747,6 +771,17 @@ impl Connection {
|
||||
if let Some(new_codec) = requested_codec {
|
||||
// Check if this codec is different from current and available
|
||||
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();
|
||||
if registry.is_format_available(new_codec, false) {
|
||||
info!(
|
||||
@@ -1080,12 +1115,21 @@ impl Connection {
|
||||
if success {
|
||||
// Dynamically detect available encoders
|
||||
let registry = EncoderRegistry::global();
|
||||
let constraints = self.current_codec_constraints().await;
|
||||
|
||||
// Check which encoders are available (include software fallback)
|
||||
let h264_available = registry.is_format_available(VideoEncoderType::H264, false);
|
||||
let h265_available = registry.is_format_available(VideoEncoderType::H265, false);
|
||||
let vp8_available = registry.is_format_available(VideoEncoderType::VP8, false);
|
||||
let vp9_available = registry.is_format_available(VideoEncoderType::VP9, false);
|
||||
let h264_available = constraints
|
||||
.is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::H264)
|
||||
&& registry.is_format_available(VideoEncoderType::H264, 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!(
|
||||
"Server encoding capabilities: H264={}, H265={}, VP8={}, VP9={}",
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::extensions::ExtensionManager;
|
||||
use crate::hid::HidController;
|
||||
use crate::msd::MsdController;
|
||||
use crate::otg::OtgService;
|
||||
use crate::rtsp::RtspService;
|
||||
use crate::rustdesk::RustDeskService;
|
||||
use crate::video::VideoStreamManager;
|
||||
|
||||
@@ -50,6 +51,8 @@ pub struct AppState {
|
||||
pub audio: Arc<AudioController>,
|
||||
/// RustDesk remote access service (optional)
|
||||
pub rustdesk: Arc<RwLock<Option<Arc<RustDeskService>>>>,
|
||||
/// RTSP streaming service (optional)
|
||||
pub rtsp: Arc<RwLock<Option<Arc<RtspService>>>>,
|
||||
/// Extension manager (ttyd, gostc, easytier)
|
||||
pub extensions: Arc<ExtensionManager>,
|
||||
/// Event bus for real-time notifications
|
||||
@@ -76,6 +79,7 @@ impl AppState {
|
||||
atx: Option<AtxController>,
|
||||
audio: Arc<AudioController>,
|
||||
rustdesk: Option<Arc<RustDeskService>>,
|
||||
rtsp: Option<Arc<RtspService>>,
|
||||
extensions: Arc<ExtensionManager>,
|
||||
events: Arc<EventBus>,
|
||||
shutdown_tx: broadcast::Sender<()>,
|
||||
@@ -92,6 +96,7 @@ impl AppState {
|
||||
atx: Arc::new(RwLock::new(atx)),
|
||||
audio,
|
||||
rustdesk: Arc::new(RwLock::new(rustdesk)),
|
||||
rtsp: Arc::new(RwLock::new(rtsp)),
|
||||
extensions,
|
||||
events,
|
||||
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.
|
||||
|
||||
pub mod capture;
|
||||
pub mod codec_constraints;
|
||||
pub mod convert;
|
||||
pub mod decoder;
|
||||
pub mod device;
|
||||
|
||||
@@ -37,6 +37,7 @@ use crate::error::Result;
|
||||
use crate::events::{EventBus, SystemEvent, VideoDeviceInfo};
|
||||
use crate::hid::HidController;
|
||||
use crate::stream::MjpegStreamHandler;
|
||||
use crate::video::codec_constraints::StreamCodecConstraints;
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
use crate::video::streamer::{Streamer, StreamerState};
|
||||
use crate::webrtc::WebRtcStreamer;
|
||||
@@ -144,6 +145,16 @@ impl VideoStreamManager {
|
||||
*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
|
||||
pub async fn current_mode(&self) -> StreamMode {
|
||||
self.mode.read().await.clone()
|
||||
|
||||
@@ -7,7 +7,11 @@ use std::sync::Arc;
|
||||
use crate::config::*;
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::events::SystemEvent;
|
||||
use crate::rtsp::RtspService;
|
||||
use crate::state::AppState;
|
||||
use crate::video::codec_constraints::{
|
||||
enforce_constraints_with_stream_manager, StreamCodecConstraints,
|
||||
};
|
||||
|
||||
/// 应用 Video 配置变更
|
||||
pub async fn apply_video_config(
|
||||
@@ -444,6 +448,15 @@ pub async fn apply_audio_config(
|
||||
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 配置变更
|
||||
pub async fn apply_rustdesk_config(
|
||||
state: &Arc<AppState>,
|
||||
@@ -453,6 +466,7 @@ pub async fn apply_rustdesk_config(
|
||||
tracing::info!("Applying RustDesk config changes...");
|
||||
|
||||
let mut rustdesk_guard = state.rustdesk.write().await;
|
||||
let mut credentials_to_save = None;
|
||||
|
||||
// Check if service needs to be stopped
|
||||
if old_config.enabled && !new_config.enabled {
|
||||
@@ -464,7 +478,6 @@ pub async fn apply_rustdesk_config(
|
||||
tracing::info!("RustDesk service stopped");
|
||||
}
|
||||
*rustdesk_guard = None;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 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_password != new_config.device_password;
|
||||
|
||||
let mut credentials_to_save = None;
|
||||
|
||||
if rustdesk_guard.is_none() {
|
||||
// Create new service
|
||||
tracing::info!("Initializing RustDesk service...");
|
||||
@@ -507,28 +518,82 @@ pub async fn apply_rustdesk_config(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save credentials to persistent config store (outside the lock)
|
||||
drop(rustdesk_guard);
|
||||
if let Some(updated_config) = credentials_to_save {
|
||||
tracing::info!("Saving RustDesk credentials to config store...");
|
||||
if let Err(e) = state
|
||||
.config
|
||||
.update(|cfg| {
|
||||
cfg.rustdesk.public_key = updated_config.public_key.clone();
|
||||
cfg.rustdesk.private_key = updated_config.private_key.clone();
|
||||
cfg.rustdesk.signing_public_key = updated_config.signing_public_key.clone();
|
||||
cfg.rustdesk.signing_private_key = updated_config.signing_private_key.clone();
|
||||
cfg.rustdesk.uuid = updated_config.uuid.clone();
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to save RustDesk credentials: {}", e);
|
||||
} else {
|
||||
tracing::info!("RustDesk credentials saved successfully");
|
||||
}
|
||||
// Save credentials to persistent config store (outside the lock)
|
||||
drop(rustdesk_guard);
|
||||
if let Some(updated_config) = credentials_to_save {
|
||||
tracing::info!("Saving RustDesk credentials to config store...");
|
||||
if let Err(e) = state
|
||||
.config
|
||||
.update(|cfg| {
|
||||
cfg.rustdesk.public_key = updated_config.public_key.clone();
|
||||
cfg.rustdesk.private_key = updated_config.private_key.clone();
|
||||
cfg.rustdesk.signing_public_key = updated_config.signing_public_key.clone();
|
||||
cfg.rustdesk.signing_private_key = updated_config.signing_private_key.clone();
|
||||
cfg.rustdesk.uuid = updated_config.uuid.clone();
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to save RustDesk credentials: {}", e);
|
||||
} else {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ mod auth;
|
||||
mod hid;
|
||||
mod msd;
|
||||
mod rustdesk;
|
||||
mod rtsp;
|
||||
mod stream;
|
||||
pub(crate) mod video;
|
||||
mod web;
|
||||
@@ -39,6 +40,7 @@ pub use rustdesk::{
|
||||
get_device_password, get_rustdesk_config, get_rustdesk_status, regenerate_device_id,
|
||||
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 video::{get_video_config, update_video_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.signing_public_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);
|
||||
}
|
||||
|
||||
// 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)))
|
||||
}
|
||||
|
||||
|
||||
@@ -42,5 +42,10 @@ pub async fn update_stream_config(
|
||||
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)))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::config::*;
|
||||
use crate::error::AppError;
|
||||
use crate::rtsp::RtspServiceStatus;
|
||||
use crate::rustdesk::config::RustDeskConfig;
|
||||
use crate::video::encoder::BitratePreset;
|
||||
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 =====
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::config::{AppConfig, StreamMode};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::events::SystemEvent;
|
||||
use crate::state::AppState;
|
||||
use crate::video::codec_constraints::codec_to_id;
|
||||
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
|
||||
if new_config.audio.enabled {
|
||||
let audio_config = crate::audio::AudioControllerConfig {
|
||||
@@ -1439,6 +1451,8 @@ pub async fn stream_mode_set(
|
||||
) -> Result<Json<StreamModeResponse>> {
|
||||
use crate::video::encoder::VideoCodecType;
|
||||
|
||||
let constraints = state.stream_manager.codec_constraints().await;
|
||||
|
||||
let mode_lower = req.mode.to_lowercase();
|
||||
let (new_mode, video_codec) = match mode_lower.as_str() {
|
||||
"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
|
||||
if let Some(codec) = video_codec {
|
||||
info!("Setting WebRTC video codec to {:?}", codec);
|
||||
@@ -1560,6 +1591,67 @@ pub struct AvailableCodecsResponse {
|
||||
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
|
||||
#[derive(Deserialize)]
|
||||
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/bitrate", post(handlers::stream_set_bitrate))
|
||||
.route("/stream/codecs", get(handlers::stream_codecs_list))
|
||||
.route("/stream/constraints", get(handlers::stream_constraints_get))
|
||||
// WebRTC endpoints
|
||||
.route("/webrtc/session", post(handlers::webrtc_create_session))
|
||||
.route("/webrtc/offer", post(handlers::webrtc_offer))
|
||||
@@ -120,6 +121,13 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
||||
"/config/rustdesk/regenerate-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
|
||||
.route("/config/web", get(handlers::config::get_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 服务器配置 */
|
||||
|
||||
@@ -124,6 +124,19 @@ export interface AvailableCodecsResponse {
|
||||
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 = {
|
||||
status: () =>
|
||||
request<{
|
||||
@@ -161,6 +174,9 @@ export const streamApi = {
|
||||
getCodecs: () =>
|
||||
request<AvailableCodecsResponse>('/stream/codecs'),
|
||||
|
||||
getConstraints: () =>
|
||||
request<StreamConstraintsResponse>('/stream/constraints'),
|
||||
|
||||
setBitratePreset: (bitrate_preset: import('@/types/generated').BitratePreset) =>
|
||||
request<{ success: boolean; message?: string }>('/stream/bitrate', {
|
||||
method: 'POST',
|
||||
@@ -536,11 +552,15 @@ export {
|
||||
audioConfigApi,
|
||||
extensionsApi,
|
||||
rustdeskConfigApi,
|
||||
rtspConfigApi,
|
||||
webConfigApi,
|
||||
type RustDeskConfigResponse,
|
||||
type RustDeskStatusResponse,
|
||||
type RustDeskConfigUpdate,
|
||||
type RustDeskPasswordResponse,
|
||||
type RtspConfigResponse,
|
||||
type RtspConfigUpdate,
|
||||
type RtspStatusResponse,
|
||||
type WebConfig,
|
||||
} from './config'
|
||||
|
||||
|
||||
@@ -19,7 +19,14 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
import { Monitor, RefreshCw, Loader2, Settings, Zap, Scale, Image } from 'lucide-vue-next'
|
||||
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 { useRouter } from 'vue-router'
|
||||
|
||||
@@ -64,6 +71,7 @@ const loadingCodecs = ref(false)
|
||||
|
||||
// Backend list
|
||||
const backends = ref<EncoderBackendInfo[]>([])
|
||||
const constraints = ref<StreamConstraintsResponse | null>(null)
|
||||
const currentEncoderBackend = computed(() => configStore.stream?.encoder || 'auto')
|
||||
|
||||
// Browser supported codecs (WebRTC receive capabilities)
|
||||
@@ -220,7 +228,7 @@ const availableCodecs = computed(() => {
|
||||
const backend = backends.value.find(b => b.id === currentEncoderBackend.value)
|
||||
if (!backend) return allAvailable
|
||||
|
||||
return allAvailable
|
||||
const backendFiltered = allAvailable
|
||||
.filter(codec => {
|
||||
// MJPEG is always available (doesn't require encoder)
|
||||
if (codec.id === 'mjpeg') return true
|
||||
@@ -238,6 +246,13 @@ const availableCodecs = computed(() => {
|
||||
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
|
||||
@@ -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)
|
||||
function goToSettings() {
|
||||
router.push('/settings?tab=video')
|
||||
@@ -339,6 +362,12 @@ function syncFromCurrentIfChanged() {
|
||||
// Handle video mode change
|
||||
function handleVideoModeChange(mode: unknown) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -466,6 +495,8 @@ watch(() => props.open, (isOpen) => {
|
||||
loadCodecs()
|
||||
}
|
||||
|
||||
loadConstraints()
|
||||
|
||||
Promise.all([
|
||||
configStore.refreshVideo(),
|
||||
configStore.refreshStream(),
|
||||
|
||||
@@ -357,6 +357,30 @@ export interface RustDeskConfig {
|
||||
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 */
|
||||
export interface AppConfig {
|
||||
/** Whether initial setup has been completed */
|
||||
@@ -381,6 +405,8 @@ export interface AppConfig {
|
||||
extensions: ExtensionsConfig;
|
||||
/** RustDesk remote access settings */
|
||||
rustdesk: RustDeskConfig;
|
||||
/** RTSP streaming settings */
|
||||
rtsp: RtspConfig;
|
||||
}
|
||||
|
||||
/** Update for a single ATX key configuration */
|
||||
@@ -557,6 +583,33 @@ export interface MsdConfigUpdate {
|
||||
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 {
|
||||
enabled?: boolean;
|
||||
rendezvous_server?: string;
|
||||
|
||||
Reference in New Issue
Block a user