mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-29 11:51:44 +08:00
feat: 新增 MJPEG/H.264 VNC 初步支持
This commit is contained in:
@@ -32,6 +32,7 @@ pub struct AppConfig {
|
||||
pub web: WebConfig,
|
||||
pub extensions: ExtensionsConfig,
|
||||
pub rustdesk: RustDeskConfig,
|
||||
pub vnc: VncConfig,
|
||||
pub rtsp: RtspConfig,
|
||||
pub redfish: RedfishConfig,
|
||||
}
|
||||
|
||||
@@ -23,6 +23,44 @@ pub enum RtspCodec {
|
||||
H265,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[derive(Default)]
|
||||
pub enum VncEncoding {
|
||||
#[default]
|
||||
TightJpeg,
|
||||
H264,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct VncConfig {
|
||||
pub enabled: bool,
|
||||
pub bind: String,
|
||||
pub port: u16,
|
||||
pub encoding: VncEncoding,
|
||||
pub jpeg_quality: u8,
|
||||
pub allow_one_client: bool,
|
||||
#[typeshare(skip)]
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for VncConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
bind: "0.0.0.0".to_string(),
|
||||
port: 5900,
|
||||
encoding: VncEncoding::TightJpeg,
|
||||
jpeg_quality: 80,
|
||||
allow_one_client: true,
|
||||
password: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
|
||||
@@ -51,6 +51,8 @@ pub mod utils;
|
||||
#[cfg(any(feature = "android", feature = "desktop"))]
|
||||
pub mod video;
|
||||
#[cfg(any(feature = "android", feature = "desktop"))]
|
||||
pub mod vnc;
|
||||
#[cfg(any(feature = "android", feature = "desktop"))]
|
||||
pub mod web;
|
||||
#[cfg(any(feature = "android", feature = "desktop"))]
|
||||
pub mod webrtc;
|
||||
|
||||
52
src/main.rs
52
src/main.rs
@@ -31,10 +31,12 @@ use one_kvm::state::{AppState, ShutdownAction};
|
||||
use one_kvm::update::UpdateService;
|
||||
use one_kvm::utils::bind_tcp_listener;
|
||||
use one_kvm::video::codec_constraints::{
|
||||
enforce_constraints_with_stream_manager, StreamCodecConstraints,
|
||||
enforce_constraints_with_stream_manager, validate_third_party_codec_compatibility,
|
||||
StreamCodecConstraints,
|
||||
};
|
||||
use one_kvm::video::format::{PixelFormat, Resolution};
|
||||
use one_kvm::video::{Streamer, VideoStreamManager};
|
||||
use one_kvm::vnc::VncService;
|
||||
use one_kvm::web;
|
||||
use one_kvm::webrtc::{WebRtcStreamer, WebRtcStreamerConfig};
|
||||
|
||||
@@ -486,7 +488,18 @@ async fn main() -> anyhow::Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
let rustdesk = if config.rustdesk.is_valid() {
|
||||
let third_party_codec_config_valid = match validate_third_party_codec_compatibility(&config) {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Third-party access codec configuration is invalid; RustDesk/VNC/RTSP will not start: {}",
|
||||
e
|
||||
);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let rustdesk = if third_party_codec_config_valid && config.rustdesk.is_valid() {
|
||||
tracing::info!(
|
||||
"Initializing RustDesk service: ID={} -> {}",
|
||||
config.rustdesk.device_id,
|
||||
@@ -510,7 +523,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
let rtsp = if config.rtsp.enabled {
|
||||
let rtsp = if third_party_codec_config_valid && config.rtsp.enabled {
|
||||
tracing::info!(
|
||||
"Initializing RTSP service: rtsp://{}:{}/{}",
|
||||
config.rtsp.bind,
|
||||
@@ -524,6 +537,23 @@ async fn main() -> anyhow::Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
let vnc = if third_party_codec_config_valid && config.vnc.enabled {
|
||||
tracing::info!(
|
||||
"Initializing VNC service: {}:{} ({:?})",
|
||||
config.vnc.bind,
|
||||
config.vnc.port,
|
||||
config.vnc.encoding
|
||||
);
|
||||
Some(Arc::new(VncService::new(
|
||||
config.vnc.clone(),
|
||||
stream_manager.clone(),
|
||||
hid.clone(),
|
||||
)))
|
||||
} else {
|
||||
tracing::info!("VNC disabled in configuration");
|
||||
None
|
||||
};
|
||||
|
||||
let update_service = Arc::new(UpdateService::new(data_dir.join("updates")));
|
||||
|
||||
let state = AppState::new(
|
||||
@@ -541,6 +571,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
atx,
|
||||
audio,
|
||||
rustdesk.clone(),
|
||||
vnc.clone(),
|
||||
rtsp.clone(),
|
||||
extensions.clone(),
|
||||
events.clone(),
|
||||
@@ -573,6 +604,13 @@ async fn main() -> anyhow::Result<()> {
|
||||
tracing::info!("RustDesk service started");
|
||||
}
|
||||
}
|
||||
if let Some(ref service) = vnc {
|
||||
if let Err(e) = service.start().await {
|
||||
tracing::error!("Failed to start VNC service: {}", e);
|
||||
} else {
|
||||
tracing::info!("VNC service started");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref service) = rtsp {
|
||||
if let Err(e) = service.start().await {
|
||||
@@ -1135,6 +1173,14 @@ async fn cleanup(state: &Arc<AppState>) {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref service) = *state.vnc.read().await {
|
||||
if let Err(e) = service.stop().await {
|
||||
tracing::warn!("Failed to stop VNC service: {}", e);
|
||||
} else {
|
||||
tracing::info!("VNC service stopped");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref service) = *state.rtsp.read().await {
|
||||
if let Err(e) = service.stop().await {
|
||||
tracing::warn!("Failed to stop RTSP service: {}", e);
|
||||
|
||||
@@ -36,6 +36,7 @@ pub fn capabilities() -> PlatformCapabilities {
|
||||
audio: FeatureCapability::available(["alsa", "opus"])
|
||||
.with_selected_backend(Some("alsa".to_string())),
|
||||
rustdesk: FeatureCapability::available(["builtin"]),
|
||||
vnc: FeatureCapability::available(["builtin", "tight_jpeg", "h264"]),
|
||||
diagnostics: FeatureCapability::available(["android_linux"]),
|
||||
extensions: FeatureCapability::unsupported("unsupported on Android Amlogic v1"),
|
||||
service_installation: FeatureCapability::available(["android_foreground_service"]),
|
||||
|
||||
@@ -78,6 +78,7 @@ pub struct PlatformCapabilities {
|
||||
pub otg: FeatureCapability,
|
||||
pub audio: FeatureCapability,
|
||||
pub rustdesk: FeatureCapability,
|
||||
pub vnc: FeatureCapability,
|
||||
pub diagnostics: FeatureCapability,
|
||||
pub extensions: FeatureCapability,
|
||||
pub service_installation: FeatureCapability,
|
||||
|
||||
@@ -16,6 +16,7 @@ pub fn capabilities() -> PlatformCapabilities {
|
||||
otg: FeatureCapability::available(["configfs"]),
|
||||
audio: FeatureCapability::available(["alsa"]),
|
||||
rustdesk: FeatureCapability::available(["builtin"]),
|
||||
vnc: FeatureCapability::available(["builtin", "tight_jpeg", "h264"]),
|
||||
diagnostics: FeatureCapability::available(["linux"]),
|
||||
extensions: FeatureCapability::available(["linux"]),
|
||||
service_installation: FeatureCapability::available(["systemd"]),
|
||||
|
||||
@@ -26,6 +26,8 @@ pub fn capabilities() -> PlatformCapabilities {
|
||||
.with_selected_backend(Some("wasapi".to_string())),
|
||||
rustdesk: FeatureCapability::available(["builtin", "tcp_direct", "relay"])
|
||||
.with_selected_backend(Some("builtin".to_string())),
|
||||
vnc: FeatureCapability::available(["builtin", "tight_jpeg", "h264"])
|
||||
.with_selected_backend(Some("builtin".to_string())),
|
||||
diagnostics: FeatureCapability::available(["windows"]),
|
||||
extensions: FeatureCapability::available(["windows_safe"]),
|
||||
service_installation: FeatureCapability::available(["windows_service"]),
|
||||
|
||||
@@ -32,10 +32,12 @@ use crate::stream_encoder::encoder_type_to_backend;
|
||||
use crate::update::UpdateService;
|
||||
use crate::utils::bind_tcp_listener;
|
||||
use crate::video::codec_constraints::{
|
||||
enforce_constraints_with_stream_manager, StreamCodecConstraints,
|
||||
enforce_constraints_with_stream_manager, validate_third_party_codec_compatibility,
|
||||
StreamCodecConstraints,
|
||||
};
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
use crate::video::{Streamer, VideoStreamManager};
|
||||
use crate::vnc::VncService;
|
||||
use crate::web;
|
||||
use crate::webrtc::{config::WebRtcConfig, WebRtcStreamer, WebRtcStreamerConfig};
|
||||
|
||||
@@ -440,7 +442,18 @@ async fn build_app_state(
|
||||
tracing::warn!("Failed to initialize Android stream manager: {}", err);
|
||||
}
|
||||
|
||||
let rustdesk = if config.rustdesk.is_valid() {
|
||||
let third_party_codec_config_valid = match validate_third_party_codec_compatibility(&config) {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Android third-party access codec configuration is invalid; RustDesk/VNC/RTSP will not start: {}",
|
||||
e
|
||||
);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let rustdesk = if third_party_codec_config_valid && config.rustdesk.is_valid() {
|
||||
Some(Arc::new(RustDeskService::new(
|
||||
config.rustdesk.clone(),
|
||||
stream_manager.clone(),
|
||||
@@ -451,7 +464,7 @@ async fn build_app_state(
|
||||
None
|
||||
};
|
||||
|
||||
let rtsp = if config.rtsp.enabled {
|
||||
let rtsp = if third_party_codec_config_valid && config.rtsp.enabled {
|
||||
Some(Arc::new(RtspService::new(
|
||||
config.rtsp.clone(),
|
||||
stream_manager.clone(),
|
||||
@@ -459,6 +472,15 @@ async fn build_app_state(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let vnc = if third_party_codec_config_valid && config.vnc.enabled {
|
||||
Some(Arc::new(VncService::new(
|
||||
config.vnc.clone(),
|
||||
stream_manager.clone(),
|
||||
hid.clone(),
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let update_service = Arc::new(UpdateService::new(data_dir.join("updates")));
|
||||
let state = AppState::new(
|
||||
@@ -474,6 +496,7 @@ async fn build_app_state(
|
||||
atx,
|
||||
audio,
|
||||
rustdesk.clone(),
|
||||
vnc.clone(),
|
||||
rtsp.clone(),
|
||||
extensions.clone(),
|
||||
events.clone(),
|
||||
@@ -489,6 +512,11 @@ async fn build_app_state(
|
||||
tracing::warn!("Failed to start Android RustDesk service: {}", err);
|
||||
}
|
||||
}
|
||||
if let Some(service) = vnc {
|
||||
if let Err(err) = service.start().await {
|
||||
tracing::warn!("Failed to start Android VNC service: {}", err);
|
||||
}
|
||||
}
|
||||
if let Some(service) = rtsp {
|
||||
if let Err(err) = service.start().await {
|
||||
tracing::warn!("Failed to start Android RTSP service: {}", err);
|
||||
@@ -674,6 +702,12 @@ async fn cleanup(state: &Arc<AppState>) {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(service) = state.vnc.read().await.as_ref() {
|
||||
if let Err(err) = service.stop().await {
|
||||
tracing::warn!("Failed to stop Android VNC service: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(service) = state.rtsp.read().await.as_ref() {
|
||||
if let Err(err) = service.stop().await {
|
||||
tracing::warn!("Failed to stop Android RTSP service: {}", err);
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[derive(Default)]
|
||||
pub enum RustDeskCodec {
|
||||
#[default]
|
||||
H264,
|
||||
H265,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct RustDeskConfig {
|
||||
pub enabled: bool,
|
||||
pub codec: RustDeskCodec,
|
||||
pub rendezvous_server: String,
|
||||
pub relay_server: Option<String>,
|
||||
#[typeshare(skip)]
|
||||
@@ -29,6 +40,7 @@ impl Default for RustDeskConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
codec: RustDeskCodec::H264,
|
||||
rendezvous_server: String::new(),
|
||||
relay_server: None,
|
||||
relay_key: None,
|
||||
|
||||
@@ -18,9 +18,7 @@ use crate::hid::{CanonicalKey, HidController, KeyEventType, KeyboardEvent, Keybo
|
||||
use crate::utils::hostname_from_etc;
|
||||
use crate::video::codec::registry::{EncoderRegistry, VideoEncoderType};
|
||||
use crate::video::codec::BitratePreset;
|
||||
use crate::video::codec_constraints::{
|
||||
encoder_codec_to_id, encoder_codec_to_video_codec, video_codec_to_encoder_codec,
|
||||
};
|
||||
use crate::video::codec_constraints::{encoder_codec_to_id, encoder_codec_to_video_codec};
|
||||
use crate::video::stream_manager::VideoStreamManager;
|
||||
|
||||
use super::bytes_codec::{read_frame, write_frame, write_frame_buffered};
|
||||
@@ -160,6 +158,8 @@ pub struct Connection {
|
||||
last_caps_lock: bool,
|
||||
/// Whether relative mouse mode is currently active for this connection
|
||||
relative_mouse_active: bool,
|
||||
/// Server-configured RustDesk video codec.
|
||||
configured_codec: VideoEncoderType,
|
||||
}
|
||||
|
||||
/// Messages sent to connection handler
|
||||
@@ -209,6 +209,11 @@ impl Connection {
|
||||
// This is used for encrypting the symmetric key exchange
|
||||
let temp_keypair = box_::gen_keypair();
|
||||
|
||||
let configured_codec = match config.codec {
|
||||
super::config::RustDeskCodec::H264 => VideoEncoderType::H264,
|
||||
super::config::RustDeskCodec::H265 => VideoEncoderType::H265,
|
||||
};
|
||||
|
||||
let conn = Self {
|
||||
id,
|
||||
device_id: config.device_id.clone(),
|
||||
@@ -238,6 +243,7 @@ impl Connection {
|
||||
last_test_delay_sent: None,
|
||||
last_caps_lock: false,
|
||||
relative_mouse_active: false,
|
||||
configured_codec,
|
||||
};
|
||||
|
||||
(conn, rx)
|
||||
@@ -628,43 +634,29 @@ impl Connection {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Negotiate video codec - select the best available encoder
|
||||
/// Priority: H264 > H265 > VP8 > VP9 (H264/H265 leverage hardware encoding on embedded devices)
|
||||
/// Negotiate video codec from the server-configured RustDesk codec.
|
||||
async fn negotiate_video_codec(&self) -> VideoEncoderType {
|
||||
let registry = EncoderRegistry::global();
|
||||
let constraints = self.current_codec_constraints().await;
|
||||
let configured = self.configured_codec;
|
||||
|
||||
// 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 constraints.is_webrtc_codec_allowed(crate::video::codec::VideoCodecType::H264)
|
||||
&& registry.is_codec_available(VideoEncoderType::H264)
|
||||
{
|
||||
return VideoEncoderType::H264;
|
||||
}
|
||||
if constraints.is_webrtc_codec_allowed(crate::video::codec::VideoCodecType::H265)
|
||||
&& registry.is_codec_available(VideoEncoderType::H265)
|
||||
{
|
||||
return VideoEncoderType::H265;
|
||||
}
|
||||
if constraints.is_webrtc_codec_allowed(crate::video::codec::VideoCodecType::VP8)
|
||||
&& registry.is_codec_available(VideoEncoderType::VP8)
|
||||
{
|
||||
return VideoEncoderType::VP8;
|
||||
}
|
||||
if constraints.is_webrtc_codec_allowed(crate::video::codec::VideoCodecType::VP9)
|
||||
&& registry.is_codec_available(VideoEncoderType::VP9)
|
||||
{
|
||||
return VideoEncoderType::VP9;
|
||||
if !constraints.is_webrtc_codec_allowed(encoder_codec_to_video_codec(configured)) {
|
||||
warn!(
|
||||
"Configured RustDesk codec {} is blocked by constraints: {}",
|
||||
encoder_codec_to_id(configured),
|
||||
constraints.reason
|
||||
);
|
||||
return configured;
|
||||
}
|
||||
|
||||
// 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)
|
||||
if !registry.is_codec_available(configured) {
|
||||
warn!(
|
||||
"Configured RustDesk codec {} is not reported available; attempting to use it anyway",
|
||||
encoder_codec_to_id(configured)
|
||||
);
|
||||
}
|
||||
|
||||
configured
|
||||
}
|
||||
|
||||
async fn current_codec_constraints(
|
||||
@@ -740,53 +732,10 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if client sent supported_decoding with a codec preference
|
||||
// Codec switching is locked to the server-configured RustDesk codec.
|
||||
if let Some(supported_decoding) = opt.supported_decoding.as_ref() {
|
||||
let prefer = supported_decoding.prefer.value();
|
||||
debug!("Client codec preference: prefer={}", prefer);
|
||||
|
||||
// Map RustDesk PreferCodec enum to our VideoEncoderType
|
||||
// From proto: Auto=0, VP9=1, H264=2, H265=3, VP8=4, AV1=5
|
||||
let requested_codec = match prefer {
|
||||
1 => Some(VideoEncoderType::VP9),
|
||||
2 => Some(VideoEncoderType::H264),
|
||||
3 => Some(VideoEncoderType::H265),
|
||||
4 => Some(VideoEncoderType::VP8),
|
||||
// Auto(0) or AV1(5) or unknown: use current or negotiate
|
||||
_ => None,
|
||||
};
|
||||
|
||||
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_codec_available(new_codec) {
|
||||
info!(
|
||||
"Client requested codec switch: {:?} -> {:?}",
|
||||
self.negotiated_codec, new_codec
|
||||
);
|
||||
// Switch codec
|
||||
if let Err(e) = self.switch_video_codec(new_codec).await {
|
||||
warn!("Failed to switch video codec: {}", e);
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"Client requested codec {:?} but it's not available",
|
||||
new_codec
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log custom_image_quality (accept but don't process)
|
||||
@@ -803,31 +752,6 @@ impl Connection {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Switch video codec dynamically
|
||||
/// Stops current video task, changes codec, and restarts
|
||||
async fn switch_video_codec(&mut self, new_codec: VideoEncoderType) -> anyhow::Result<()> {
|
||||
// Stop current video streaming task
|
||||
if let Some(task) = self.video_task.take() {
|
||||
info!("Stopping video task for codec switch");
|
||||
task.abort();
|
||||
// Wait a bit for cleanup
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
}
|
||||
|
||||
// Update negotiated codec
|
||||
self.negotiated_codec = Some(new_codec);
|
||||
|
||||
// Restart video streaming with new codec if we have a video_tx
|
||||
if let Some(ref video_tx) = self.video_frame_tx {
|
||||
info!("Restarting video streaming with codec {:?}", new_codec);
|
||||
self.start_video_streaming(video_tx.clone());
|
||||
} else {
|
||||
warn!("No video_tx available, cannot restart video streaming");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start video streaming task
|
||||
fn start_video_streaming(&mut self, video_tx: mpsc::Sender<Bytes>) {
|
||||
let video_manager = match &self.video_manager {
|
||||
@@ -1105,18 +1029,15 @@ impl Connection {
|
||||
let constraints = self.current_codec_constraints().await;
|
||||
|
||||
// Check which encoders are available (include software fallback)
|
||||
let h264_available = constraints
|
||||
.is_webrtc_codec_allowed(crate::video::codec::VideoCodecType::H264)
|
||||
let configured = self.configured_codec;
|
||||
let h264_available = configured == VideoEncoderType::H264
|
||||
&& constraints.is_webrtc_codec_allowed(crate::video::codec::VideoCodecType::H264)
|
||||
&& registry.is_codec_available(VideoEncoderType::H264);
|
||||
let h265_available = constraints
|
||||
.is_webrtc_codec_allowed(crate::video::codec::VideoCodecType::H265)
|
||||
let h265_available = configured == VideoEncoderType::H265
|
||||
&& constraints.is_webrtc_codec_allowed(crate::video::codec::VideoCodecType::H265)
|
||||
&& registry.is_codec_available(VideoEncoderType::H265);
|
||||
let vp8_available = constraints
|
||||
.is_webrtc_codec_allowed(crate::video::codec::VideoCodecType::VP8)
|
||||
&& registry.is_codec_available(VideoEncoderType::VP8);
|
||||
let vp9_available = constraints
|
||||
.is_webrtc_codec_allowed(crate::video::codec::VideoCodecType::VP9)
|
||||
&& registry.is_codec_available(VideoEncoderType::VP9);
|
||||
let vp8_available = false;
|
||||
let vp9_available = false;
|
||||
|
||||
info!(
|
||||
"Server encoding capabilities: H264={}, H265={}, VP8={}, VP9={}",
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::rtsp::RtspService;
|
||||
use crate::rustdesk::RustDeskService;
|
||||
use crate::update::UpdateService;
|
||||
use crate::video::VideoStreamManager;
|
||||
use crate::vnc::VncService;
|
||||
use crate::webrtc::WebRtcStreamer;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -30,6 +31,7 @@ pub struct ConfigApplyLocks {
|
||||
pub audio: Arc<Mutex<()>>,
|
||||
pub atx: Arc<Mutex<()>>,
|
||||
pub rustdesk: Arc<Mutex<()>>,
|
||||
pub vnc: Arc<Mutex<()>>,
|
||||
pub rtsp: Arc<Mutex<()>>,
|
||||
}
|
||||
|
||||
@@ -48,6 +50,7 @@ impl ConfigApplyLocks {
|
||||
audio: Arc::new(Mutex::new(())),
|
||||
atx: Arc::new(Mutex::new(())),
|
||||
rustdesk: Arc::new(Mutex::new(())),
|
||||
vnc: Arc::new(Mutex::new(())),
|
||||
rtsp: Arc::new(Mutex::new(())),
|
||||
}
|
||||
}
|
||||
@@ -69,6 +72,7 @@ pub struct AppState {
|
||||
pub atx: Arc<RwLock<Option<AtxController>>>,
|
||||
pub audio: Arc<AudioController>,
|
||||
pub rustdesk: Arc<RwLock<Option<Arc<RustDeskService>>>>,
|
||||
pub vnc: Arc<RwLock<Option<Arc<VncService>>>>,
|
||||
pub rtsp: Arc<RwLock<Option<Arc<RtspService>>>>,
|
||||
pub extensions: Arc<ExtensionManager>,
|
||||
pub events: Arc<EventBus>,
|
||||
@@ -95,6 +99,7 @@ impl AppState {
|
||||
atx: Option<AtxController>,
|
||||
audio: Arc<AudioController>,
|
||||
rustdesk: Option<Arc<RustDeskService>>,
|
||||
vnc: Option<Arc<VncService>>,
|
||||
rtsp: Option<Arc<RtspService>>,
|
||||
extensions: Arc<ExtensionManager>,
|
||||
events: Arc<EventBus>,
|
||||
@@ -119,6 +124,7 @@ impl AppState {
|
||||
atx: Arc::new(RwLock::new(atx)),
|
||||
audio,
|
||||
rustdesk: Arc::new(RwLock::new(rustdesk)),
|
||||
vnc: Arc::new(RwLock::new(vnc)),
|
||||
rtsp: Arc::new(RwLock::new(rtsp)),
|
||||
extensions,
|
||||
events,
|
||||
|
||||
@@ -396,15 +396,29 @@ impl MjpegStreamHandler {
|
||||
}
|
||||
|
||||
pub fn disconnect_all_clients(&self) {
|
||||
self.disconnect_clients_matching(|_| true);
|
||||
}
|
||||
|
||||
pub fn disconnect_non_vnc_clients(&self) {
|
||||
self.disconnect_clients_matching(|id| !id.starts_with("vnc-"));
|
||||
}
|
||||
|
||||
fn disconnect_clients_matching(&self, should_disconnect: impl Fn(&str) -> bool) {
|
||||
let count = {
|
||||
let mut clients = self.clients.write();
|
||||
let count = clients.len();
|
||||
clients.clear();
|
||||
count
|
||||
let before = clients.len();
|
||||
clients.retain(|id, _| !should_disconnect(id));
|
||||
before - clients.len()
|
||||
};
|
||||
let remaining = self.client_count();
|
||||
if count > 0 {
|
||||
info!("Disconnected all {} MJPEG clients for config change", count);
|
||||
info!(
|
||||
"Disconnected {} MJPEG clients for config change (remaining: {})",
|
||||
count, remaining
|
||||
);
|
||||
}
|
||||
// Wake all subscribers. HTTP MJPEG clients will close, while persistent
|
||||
// consumers such as VNC wait for the next frame after capture restarts.
|
||||
self.set_offline();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::config::{AppConfig, RtspCodec, StreamMode};
|
||||
use crate::error::Result;
|
||||
use crate::config::{AppConfig, RtspCodec, StreamMode, VncEncoding};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::rustdesk::config::RustDeskCodec;
|
||||
use crate::video::codec::registry::VideoEncoderType;
|
||||
use crate::video::codec::VideoCodecType;
|
||||
use crate::video::VideoStreamManager;
|
||||
@@ -9,6 +10,7 @@ use std::sync::Arc;
|
||||
pub struct StreamCodecConstraints {
|
||||
pub rustdesk_enabled: bool,
|
||||
pub rtsp_enabled: bool,
|
||||
pub vnc_enabled: bool,
|
||||
pub allowed_webrtc_codecs: Vec<VideoCodecType>,
|
||||
pub allow_mjpeg: bool,
|
||||
pub locked_codec: Option<VideoCodecType>,
|
||||
@@ -21,11 +23,37 @@ pub struct ConstraintEnforcementResult {
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ThirdPartyCodecLock {
|
||||
H26x(VideoCodecType),
|
||||
Mjpeg,
|
||||
}
|
||||
|
||||
impl ThirdPartyCodecLock {
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
ThirdPartyCodecLock::H26x(codec) => codec_to_id(codec),
|
||||
ThirdPartyCodecLock::Mjpeg => "mjpeg",
|
||||
}
|
||||
}
|
||||
|
||||
fn compatible_with(self, other: Self) -> bool {
|
||||
self == other
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct ThirdPartySourceLock {
|
||||
source: &'static str,
|
||||
lock: ThirdPartyCodecLock,
|
||||
}
|
||||
|
||||
impl StreamCodecConstraints {
|
||||
pub fn unrestricted() -> Self {
|
||||
Self {
|
||||
rustdesk_enabled: false,
|
||||
rtsp_enabled: false,
|
||||
vnc_enabled: false,
|
||||
allowed_webrtc_codecs: vec![
|
||||
VideoCodecType::H264,
|
||||
VideoCodecType::H265,
|
||||
@@ -41,42 +69,39 @@ impl StreamCodecConstraints {
|
||||
pub fn from_config(config: &AppConfig) -> Self {
|
||||
let rustdesk_enabled = config.rustdesk.enabled;
|
||||
let rtsp_enabled = config.rtsp.enabled;
|
||||
let vnc_enabled = config.vnc.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)
|
||||
let locks = third_party_locks(config);
|
||||
if let Some(first) = locks.first() {
|
||||
let sources = locks
|
||||
.iter()
|
||||
.map(|item| item.source)
|
||||
.collect::<Vec<_>>()
|
||||
.join("/");
|
||||
let reason = format!(
|
||||
"{} enabled with codec lock ({})",
|
||||
sources,
|
||||
first.lock.label()
|
||||
);
|
||||
return match first.lock {
|
||||
ThirdPartyCodecLock::H26x(codec) => Self {
|
||||
rustdesk_enabled,
|
||||
rtsp_enabled,
|
||||
vnc_enabled,
|
||||
allowed_webrtc_codecs: vec![codec],
|
||||
allow_mjpeg: false,
|
||||
locked_codec: Some(codec),
|
||||
reason,
|
||||
},
|
||||
ThirdPartyCodecLock::Mjpeg => Self {
|
||||
rustdesk_enabled,
|
||||
rtsp_enabled,
|
||||
vnc_enabled,
|
||||
allowed_webrtc_codecs: vec![],
|
||||
allow_mjpeg: true,
|
||||
locked_codec: None,
|
||||
reason,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,6 +138,87 @@ impl StreamCodecConstraints {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rustdesk_codec_to_video(codec: RustDeskCodec) -> VideoCodecType {
|
||||
match codec {
|
||||
RustDeskCodec::H264 => VideoCodecType::H264,
|
||||
RustDeskCodec::H265 => VideoCodecType::H265,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rtsp_codec_to_video_codec(codec: RtspCodec) -> VideoCodecType {
|
||||
match codec {
|
||||
RtspCodec::H264 => VideoCodecType::H264,
|
||||
RtspCodec::H265 => VideoCodecType::H265,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vnc_encoding_to_video_codec(encoding: VncEncoding) -> Option<VideoCodecType> {
|
||||
match encoding {
|
||||
VncEncoding::TightJpeg => None,
|
||||
VncEncoding::H264 => Some(VideoCodecType::H264),
|
||||
}
|
||||
}
|
||||
|
||||
fn rustdesk_lock(config: &AppConfig) -> Option<ThirdPartySourceLock> {
|
||||
if config.rustdesk.enabled {
|
||||
return Some(ThirdPartySourceLock {
|
||||
source: "RustDesk",
|
||||
lock: ThirdPartyCodecLock::H26x(rustdesk_codec_to_video(config.rustdesk.codec)),
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn rtsp_lock(config: &AppConfig) -> Option<ThirdPartySourceLock> {
|
||||
if config.rtsp.enabled {
|
||||
return Some(ThirdPartySourceLock {
|
||||
source: "RTSP",
|
||||
lock: ThirdPartyCodecLock::H26x(rtsp_codec_to_video_codec(config.rtsp.codec.clone())),
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn vnc_lock(config: &AppConfig) -> Option<ThirdPartySourceLock> {
|
||||
if config.vnc.enabled {
|
||||
let lock = match config.vnc.encoding {
|
||||
VncEncoding::TightJpeg => ThirdPartyCodecLock::Mjpeg,
|
||||
VncEncoding::H264 => ThirdPartyCodecLock::H26x(VideoCodecType::H264),
|
||||
};
|
||||
return Some(ThirdPartySourceLock {
|
||||
source: "VNC",
|
||||
lock,
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn third_party_locks(config: &AppConfig) -> Vec<ThirdPartySourceLock> {
|
||||
[rustdesk_lock(config), rtsp_lock(config), vnc_lock(config)]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn validate_third_party_codec_compatibility(config: &AppConfig) -> Result<()> {
|
||||
let locks = third_party_locks(config);
|
||||
if let Some(first) = locks.first() {
|
||||
for item in locks.iter().skip(1) {
|
||||
if !first.lock.compatible_with(item.lock) {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"{} codec {} conflicts with {} codec {}; choose a compatible codec or stop the running service first",
|
||||
item.source,
|
||||
item.lock.label(),
|
||||
first.source,
|
||||
first.lock.label()
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn enforce_constraints_with_stream_manager(
|
||||
stream_manager: &Arc<VideoStreamManager>,
|
||||
constraints: &StreamCodecConstraints,
|
||||
@@ -135,6 +241,16 @@ pub async fn enforce_constraints_with_stream_manager(
|
||||
}
|
||||
|
||||
if current_mode == StreamMode::WebRTC {
|
||||
if constraints.allow_mjpeg && constraints.allowed_webrtc_codecs.is_empty() {
|
||||
let _ = stream_manager
|
||||
.switch_mode_transaction(StreamMode::Mjpeg)
|
||||
.await?;
|
||||
return Ok(ConstraintEnforcementResult {
|
||||
changed: true,
|
||||
message: Some("Auto-switched from WebRTC to MJPEG due to codec lock".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
let current_codec = stream_manager.current_video_codec().await;
|
||||
if !constraints.is_webrtc_codec_allowed(current_codec) {
|
||||
let target_codec = constraints.preferred_webrtc_codec();
|
||||
|
||||
@@ -375,8 +375,8 @@ impl Streamer {
|
||||
|
||||
// IMPORTANT: Disconnect all MJPEG clients FIRST before stopping capture
|
||||
// This prevents race conditions where clients try to reconnect and reopen the device
|
||||
debug!("Disconnecting all MJPEG clients before config change...");
|
||||
self.mjpeg_handler.disconnect_all_clients();
|
||||
debug!("Disconnecting HTTP MJPEG clients before config change...");
|
||||
self.mjpeg_handler.disconnect_non_vnc_clients();
|
||||
|
||||
// Give clients time to receive the disconnect signal and close their connections
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
|
||||
370
src/vnc/mod.rs
Normal file
370
src/vnc/mod.rs
Normal file
@@ -0,0 +1,370 @@
|
||||
//! Minimal VNC/RFB service for direct JPEG/H264 frame forwarding.
|
||||
|
||||
pub mod rfb;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::Bytes;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::{broadcast, Mutex, RwLock};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::config::{VncConfig, VncEncoding};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::hid::HidController;
|
||||
use crate::stream::mjpeg::ClientGuard;
|
||||
use crate::video::codec::{BitratePreset, VideoCodecType};
|
||||
use crate::video::stream_manager::VideoStreamManager;
|
||||
|
||||
use self::rfb::{RfbClient, RfbFrame, RfbInputEvent};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum VncServiceStatus {
|
||||
Stopped,
|
||||
Starting,
|
||||
Running,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VncServiceStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Stopped => write!(f, "stopped"),
|
||||
Self::Starting => write!(f, "starting"),
|
||||
Self::Running => write!(f, "running"),
|
||||
Self::Error(err) => write!(f, "error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VncService {
|
||||
config: Arc<RwLock<VncConfig>>,
|
||||
status: Arc<RwLock<VncServiceStatus>>,
|
||||
video_manager: Arc<VideoStreamManager>,
|
||||
hid: Arc<HidController>,
|
||||
shutdown_tx: broadcast::Sender<()>,
|
||||
server_handle: Mutex<Option<JoinHandle<()>>>,
|
||||
client_handles: Arc<Mutex<Vec<JoinHandle<()>>>>,
|
||||
active_clients: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl VncService {
|
||||
pub fn new(
|
||||
config: VncConfig,
|
||||
video_manager: Arc<VideoStreamManager>,
|
||||
hid: Arc<HidController>,
|
||||
) -> Self {
|
||||
let (shutdown_tx, _) = broadcast::channel(1);
|
||||
Self {
|
||||
config: Arc::new(RwLock::new(config)),
|
||||
status: Arc::new(RwLock::new(VncServiceStatus::Stopped)),
|
||||
video_manager,
|
||||
hid,
|
||||
shutdown_tx,
|
||||
server_handle: Mutex::new(None),
|
||||
client_handles: Arc::new(Mutex::new(Vec::new())),
|
||||
active_clients: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn config(&self) -> VncConfig {
|
||||
self.config.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn update_config(&self, config: VncConfig) {
|
||||
*self.config.write().await = config;
|
||||
}
|
||||
|
||||
pub async fn status(&self) -> VncServiceStatus {
|
||||
self.status.read().await.clone()
|
||||
}
|
||||
|
||||
pub fn connection_count(&self) -> usize {
|
||||
self.active_clients.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> Result<()> {
|
||||
let config = self.config.read().await.clone();
|
||||
if !config.enabled {
|
||||
*self.status.write().await = VncServiceStatus::Stopped;
|
||||
return Ok(());
|
||||
}
|
||||
if matches!(*self.status.read().await, VncServiceStatus::Running) {
|
||||
return Ok(());
|
||||
}
|
||||
if config.password.as_deref().unwrap_or("").is_empty() {
|
||||
let msg = "VNC password is required".to_string();
|
||||
*self.status.write().await = VncServiceStatus::Error(msg.clone());
|
||||
return Err(AppError::BadRequest(msg));
|
||||
}
|
||||
|
||||
*self.status.write().await = VncServiceStatus::Starting;
|
||||
if let Err(err) = self.prepare_video_pipeline(&config).await {
|
||||
*self.status.write().await = VncServiceStatus::Error(err.to_string());
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let bind_addr: SocketAddr = format!("{}:{}", config.bind, config.port)
|
||||
.parse()
|
||||
.map_err(|e| AppError::BadRequest(format!("Invalid VNC bind address: {}", e)))?;
|
||||
let listener = TcpListener::bind(bind_addr).await.map_err(|e| {
|
||||
AppError::Io(std::io::Error::new(
|
||||
e.kind(),
|
||||
format!("VNC bind failed: {}", e),
|
||||
))
|
||||
})?;
|
||||
|
||||
let config_ref = self.config.clone();
|
||||
let video_manager = self.video_manager.clone();
|
||||
let hid = self.hid.clone();
|
||||
let status = self.status.clone();
|
||||
let client_handles = self.client_handles.clone();
|
||||
let active_clients = self.active_clients.clone();
|
||||
let mut shutdown_rx = self.shutdown_tx.subscribe();
|
||||
|
||||
*self.status.write().await = VncServiceStatus::Running;
|
||||
let handle = tokio::spawn(async move {
|
||||
info!("VNC service listening on {}", bind_addr);
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown_rx.recv() => {
|
||||
info!("VNC service shutdown signal received");
|
||||
break;
|
||||
}
|
||||
result = listener.accept() => {
|
||||
match result {
|
||||
Ok((stream, peer)) => {
|
||||
let cfg = config_ref.read().await.clone();
|
||||
if cfg.allow_one_client && active_clients.load(Ordering::Relaxed) > 0 {
|
||||
warn!("Rejecting VNC client {} because another client is active", peer);
|
||||
drop(stream);
|
||||
continue;
|
||||
}
|
||||
let vm = video_manager.clone();
|
||||
let hid = hid.clone();
|
||||
let active = active_clients.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
active.fetch_add(1, Ordering::Relaxed);
|
||||
let result = handle_client(stream, peer, cfg, vm, hid).await;
|
||||
active.fetch_sub(1, Ordering::Relaxed);
|
||||
if let Err(err) = result {
|
||||
warn!("VNC client {} ended: {}", peer, err);
|
||||
}
|
||||
});
|
||||
let mut handles = client_handles.lock().await;
|
||||
handles.retain(|task| !task.is_finished());
|
||||
handles.push(handle);
|
||||
}
|
||||
Err(err) => warn!("VNC accept failed: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*status.write().await = VncServiceStatus::Stopped;
|
||||
});
|
||||
|
||||
*self.server_handle.lock().await = Some(handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn prepare_video_pipeline(&self, config: &VncConfig) -> Result<()> {
|
||||
match config.encoding {
|
||||
VncEncoding::TightJpeg => {
|
||||
self.video_manager
|
||||
.set_bitrate_preset(BitratePreset::Balanced)
|
||||
.await?;
|
||||
}
|
||||
VncEncoding::H264 => {
|
||||
self.video_manager
|
||||
.set_video_codec(VideoCodecType::H264)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop(&self) -> Result<()> {
|
||||
let _ = self.shutdown_tx.send(());
|
||||
if let Some(mut handle) = self.server_handle.lock().await.take() {
|
||||
match tokio::time::timeout(Duration::from_secs(2), &mut handle).await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(err)) if err.is_cancelled() => {}
|
||||
Ok(Err(err)) => warn!("VNC server task ended with error: {}", err),
|
||||
Err(_) => {
|
||||
warn!("Timed out waiting for VNC server task to stop");
|
||||
handle.abort();
|
||||
let _ = handle.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut client_handles = self.client_handles.lock().await;
|
||||
for handle in client_handles.drain(..) {
|
||||
handle.abort();
|
||||
}
|
||||
self.active_clients.store(0, Ordering::Relaxed);
|
||||
*self.status.write().await = VncServiceStatus::Stopped;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn restart(&self, config: VncConfig) -> Result<()> {
|
||||
self.update_config(config).await;
|
||||
self.stop().await?;
|
||||
self.start().await
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_client(
|
||||
stream: TcpStream,
|
||||
peer: SocketAddr,
|
||||
config: VncConfig,
|
||||
video_manager: Arc<VideoStreamManager>,
|
||||
hid: Arc<HidController>,
|
||||
) -> Result<()> {
|
||||
let mut client = RfbClient::new(stream, peer, config.clone());
|
||||
let (width, height) = initial_frame_size(&config, &video_manager).await;
|
||||
client.set_size(width, height);
|
||||
client.handshake().await?;
|
||||
let (_, _, mut frame_rx) = subscribe_frames(&config, &video_manager).await?;
|
||||
let mut shutdown = client.shutdown_receiver();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = client.read_input_event() => {
|
||||
match result? {
|
||||
RfbInputEvent::Ignored => {}
|
||||
RfbInputEvent::Disconnected => break,
|
||||
event => handle_input_event(event, &hid, width, height).await?,
|
||||
}
|
||||
}
|
||||
maybe_frame = frame_rx.recv() => {
|
||||
let Some(frame) = maybe_frame else { break };
|
||||
client.send_frame(frame).await?;
|
||||
}
|
||||
_ = shutdown.recv() => break,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn initial_frame_size(
|
||||
config: &VncConfig,
|
||||
video_manager: &Arc<VideoStreamManager>,
|
||||
) -> (u16, u16) {
|
||||
match config.encoding {
|
||||
VncEncoding::TightJpeg => {
|
||||
let (_, resolution, _, _, _) = video_manager.streamer().current_capture_config().await;
|
||||
(resolution.width as u16, resolution.height as u16)
|
||||
}
|
||||
VncEncoding::H264 => video_manager
|
||||
.get_encoding_config()
|
||||
.await
|
||||
.map(|cfg| (cfg.resolution.width as u16, cfg.resolution.height as u16))
|
||||
.unwrap_or((1280, 720)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn subscribe_frames(
|
||||
config: &VncConfig,
|
||||
video_manager: &Arc<VideoStreamManager>,
|
||||
) -> Result<(u16, u16, tokio::sync::mpsc::Receiver<RfbFrame>)> {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(4);
|
||||
match config.encoding {
|
||||
VncEncoding::TightJpeg => {
|
||||
let handler = video_manager.mjpeg_handler();
|
||||
let client_id = format!("vnc-{}", uuid::Uuid::new_v4());
|
||||
let guard = ClientGuard::new(client_id.clone(), handler.clone());
|
||||
video_manager.streamer().start().await?;
|
||||
let current = handler.current_frame();
|
||||
let (width, height) = current
|
||||
.as_ref()
|
||||
.map(|f| (f.width() as u16, f.height() as u16))
|
||||
.unwrap_or((800, 600));
|
||||
let mut notify = handler.subscribe();
|
||||
tokio::spawn(async move {
|
||||
let _guard = guard;
|
||||
loop {
|
||||
if notify.recv().await.is_err() {
|
||||
break;
|
||||
}
|
||||
let Some(frame) = handler.current_frame() else {
|
||||
continue;
|
||||
};
|
||||
if !frame.online || !frame.is_valid_jpeg() {
|
||||
continue;
|
||||
}
|
||||
let _ = tx
|
||||
.send(RfbFrame::Jpeg {
|
||||
data: frame.data_bytes(),
|
||||
width: frame.width() as u16,
|
||||
height: frame.height() as u16,
|
||||
})
|
||||
.await;
|
||||
handler.record_frame_sent(&client_id);
|
||||
}
|
||||
});
|
||||
Ok((width, height, rx))
|
||||
}
|
||||
VncEncoding::H264 => {
|
||||
video_manager.set_video_codec(VideoCodecType::H264).await?;
|
||||
let mut frames = video_manager
|
||||
.subscribe_encoded_frames()
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
AppError::VideoError("Failed to subscribe to encoded frames".to_string())
|
||||
})?;
|
||||
let geometry = video_manager
|
||||
.get_encoding_config()
|
||||
.await
|
||||
.map(|cfg| cfg.resolution)
|
||||
.unwrap_or(crate::video::format::Resolution::HD720);
|
||||
let width = geometry.width as u16;
|
||||
let height = geometry.height as u16;
|
||||
if let Err(err) = video_manager.request_keyframe().await {
|
||||
warn!("Failed to request VNC H264 keyframe: {}", err);
|
||||
}
|
||||
tokio::spawn(async move {
|
||||
while let Some(frame) = frames.recv().await {
|
||||
if frame.codec != crate::video::codec::registry::VideoEncoderType::H264 {
|
||||
continue;
|
||||
}
|
||||
let _ = tx
|
||||
.send(RfbFrame::H264 {
|
||||
data: Bytes::copy_from_slice(&frame.data),
|
||||
width,
|
||||
height,
|
||||
key: frame.is_keyframe,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
Ok((width, height, rx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_input_event(
|
||||
event: RfbInputEvent,
|
||||
hid: &Arc<HidController>,
|
||||
width: u16,
|
||||
height: u16,
|
||||
) -> Result<()> {
|
||||
match event {
|
||||
RfbInputEvent::Key(key) => {
|
||||
if let Some(event) = rfb::key_event_to_hid(key) {
|
||||
hid.send_keyboard(event).await?;
|
||||
}
|
||||
}
|
||||
RfbInputEvent::Pointer(pointer) => {
|
||||
for event in rfb::pointer_event_to_hid(pointer, width, height) {
|
||||
hid.send_mouse(event).await?;
|
||||
}
|
||||
}
|
||||
RfbInputEvent::Clipboard(_) => {}
|
||||
RfbInputEvent::Ignored | RfbInputEvent::Disconnected => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
529
src/vnc/rfb.rs
Normal file
529
src/vnc/rfb.rs
Normal file
@@ -0,0 +1,529 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use bytes::Bytes;
|
||||
use des::cipher::{BlockEncrypt, KeyInit};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::config::{VncConfig, VncEncoding};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::hid::{
|
||||
CanonicalKey, KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent,
|
||||
MouseEventType,
|
||||
};
|
||||
|
||||
const ENCODING_TIGHT: i32 = 7;
|
||||
const ENCODING_H264: i32 = 50;
|
||||
const ENCODING_DESKTOP_SIZE: i32 = -223;
|
||||
|
||||
pub enum RfbFrame {
|
||||
Jpeg {
|
||||
data: Bytes,
|
||||
width: u16,
|
||||
height: u16,
|
||||
},
|
||||
H264 {
|
||||
data: Bytes,
|
||||
width: u16,
|
||||
height: u16,
|
||||
key: bool,
|
||||
},
|
||||
}
|
||||
|
||||
pub enum RfbInputEvent {
|
||||
Key(RfbKeyEvent),
|
||||
Pointer(RfbPointerEvent),
|
||||
Clipboard(String),
|
||||
Ignored,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
pub struct RfbKeyEvent {
|
||||
pub down: bool,
|
||||
pub keysym: u32,
|
||||
}
|
||||
|
||||
pub struct RfbPointerEvent {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
pub button_mask: u8,
|
||||
pub previous_button_mask: u8,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClientEncodings {
|
||||
has_tight: bool,
|
||||
tight_jpeg_quality: u8,
|
||||
has_h264: bool,
|
||||
has_resize: bool,
|
||||
}
|
||||
|
||||
pub struct RfbClient {
|
||||
stream: TcpStream,
|
||||
peer: SocketAddr,
|
||||
config: VncConfig,
|
||||
encodings: ClientEncodings,
|
||||
width: u16,
|
||||
height: u16,
|
||||
last_buttons: u8,
|
||||
h264_waiting_keyframe: bool,
|
||||
shutdown_tx: broadcast::Sender<()>,
|
||||
}
|
||||
|
||||
impl RfbClient {
|
||||
pub fn new(stream: TcpStream, peer: SocketAddr, config: VncConfig) -> Self {
|
||||
let (shutdown_tx, _) = broadcast::channel(1);
|
||||
Self {
|
||||
stream,
|
||||
peer,
|
||||
config,
|
||||
encodings: ClientEncodings::default(),
|
||||
width: 800,
|
||||
height: 600,
|
||||
last_buttons: 0,
|
||||
h264_waiting_keyframe: true,
|
||||
shutdown_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_size(&mut self, width: u16, height: u16) {
|
||||
self.width = width.max(1);
|
||||
self.height = height.max(1);
|
||||
}
|
||||
|
||||
pub fn shutdown_receiver(&self) -> broadcast::Receiver<()> {
|
||||
self.shutdown_tx.subscribe()
|
||||
}
|
||||
|
||||
pub async fn handshake(&mut self) -> Result<()> {
|
||||
self.stream.write_all(b"RFB 003.008\n").await?;
|
||||
let mut version = [0u8; 12];
|
||||
self.stream.read_exact(&mut version).await?;
|
||||
if !version.starts_with(b"RFB 003.00") {
|
||||
return Err(AppError::BadRequest("Invalid RFB version".to_string()));
|
||||
}
|
||||
|
||||
self.stream.write_all(&[1, 2]).await?;
|
||||
let sec_type = read_u8(&mut self.stream).await?;
|
||||
if sec_type != 2 {
|
||||
return Err(AppError::BadRequest("VNCAuth is required".to_string()));
|
||||
}
|
||||
self.handle_vnc_auth().await?;
|
||||
|
||||
let _shared = read_u8(&mut self.stream).await?;
|
||||
self.write_server_init().await?;
|
||||
self.read_until_set_encodings().await?;
|
||||
self.validate_encoding_policy()?;
|
||||
tracing::info!(
|
||||
"VNC client {} negotiated encoding {:?}",
|
||||
self.peer,
|
||||
self.config.encoding
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_vnc_auth(&mut self) -> Result<()> {
|
||||
let challenge: [u8; 16] = rand::random();
|
||||
self.stream.write_all(&challenge).await?;
|
||||
let mut response = [0u8; 16];
|
||||
self.stream.read_exact(&mut response).await?;
|
||||
let password = self.config.password.as_deref().unwrap_or("");
|
||||
let expected = encrypt_vnc_challenge(&challenge, password)?;
|
||||
let ok = response == expected;
|
||||
self.stream
|
||||
.write_all(&(if ok { 0u32 } else { 1u32 }).to_be_bytes())
|
||||
.await?;
|
||||
if !ok {
|
||||
return Err(AppError::BadRequest("Invalid VNC password".to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_server_init(&mut self) -> Result<()> {
|
||||
self.stream.write_all(&self.width.to_be_bytes()).await?;
|
||||
self.stream.write_all(&self.height.to_be_bytes()).await?;
|
||||
self.stream
|
||||
.write_all(&[32, 24, 0, 1, 0, 255, 0, 255, 0, 255, 16, 8, 0, 0, 0, 0])
|
||||
.await?;
|
||||
let name = b"One-KVM VNC";
|
||||
self.stream
|
||||
.write_all(&(name.len() as u32).to_be_bytes())
|
||||
.await?;
|
||||
self.stream.write_all(name).await?;
|
||||
self.stream.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_until_set_encodings(&mut self) -> Result<()> {
|
||||
loop {
|
||||
let msg_type = read_u8(&mut self.stream).await?;
|
||||
match msg_type {
|
||||
0 => {
|
||||
let mut buf = [0u8; 19];
|
||||
self.stream.read_exact(&mut buf).await?;
|
||||
}
|
||||
2 => {
|
||||
let _pad = read_u8(&mut self.stream).await?;
|
||||
let count = read_u16(&mut self.stream).await?;
|
||||
if count == 0 || count > 1024 {
|
||||
return Err(AppError::BadRequest(
|
||||
"Invalid VNC encoding list".to_string(),
|
||||
));
|
||||
}
|
||||
let mut encodings = ClientEncodings::default();
|
||||
for _ in 0..count {
|
||||
let enc = read_i32(&mut self.stream).await?;
|
||||
match enc {
|
||||
ENCODING_TIGHT => encodings.has_tight = true,
|
||||
ENCODING_H264 => encodings.has_h264 = true,
|
||||
ENCODING_DESKTOP_SIZE => encodings.has_resize = true,
|
||||
-32..=-23 => {
|
||||
let q = ((enc + 33) * 10).clamp(10, 100) as u8;
|
||||
encodings.tight_jpeg_quality = encodings.tight_jpeg_quality.max(q);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.encodings = encodings;
|
||||
return Ok(());
|
||||
}
|
||||
3 => {
|
||||
let mut buf = [0u8; 9];
|
||||
self.stream.read_exact(&mut buf).await?;
|
||||
}
|
||||
4 => {
|
||||
let mut buf = [0u8; 7];
|
||||
self.stream.read_exact(&mut buf).await?;
|
||||
}
|
||||
5 => {
|
||||
let mut buf = [0u8; 5];
|
||||
self.stream.read_exact(&mut buf).await?;
|
||||
}
|
||||
6 => {
|
||||
let mut hdr = [0u8; 7];
|
||||
self.stream.read_exact(&mut hdr).await?;
|
||||
let len = u32::from_be_bytes([hdr[3], hdr[4], hdr[5], hdr[6]]) as usize;
|
||||
let mut data = vec![0u8; len.min(1024 * 1024)];
|
||||
self.stream.read_exact(&mut data).await?;
|
||||
}
|
||||
_ => {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"Unsupported RFB message {}",
|
||||
msg_type
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_encoding_policy(&self) -> Result<()> {
|
||||
match self.config.encoding {
|
||||
VncEncoding::TightJpeg => {
|
||||
if !self.encodings.has_tight || self.encodings.tight_jpeg_quality == 0 {
|
||||
return Err(AppError::BadRequest(
|
||||
"VNC client must support Tight JPEG encoding".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
VncEncoding::H264 => {
|
||||
if !self.encodings.has_h264 {
|
||||
return Err(AppError::BadRequest(
|
||||
"VNC client must support Open H.264 encoding".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read_input_event(&mut self) -> Result<RfbInputEvent> {
|
||||
let msg_type = match read_u8(&mut self.stream).await {
|
||||
Ok(v) => v,
|
||||
Err(AppError::Io(err)) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||
return Ok(RfbInputEvent::Disconnected);
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
match msg_type {
|
||||
0 => {
|
||||
let mut buf = [0u8; 19];
|
||||
self.stream.read_exact(&mut buf).await?;
|
||||
Ok(RfbInputEvent::Ignored)
|
||||
}
|
||||
2 => {
|
||||
let _pad = read_u8(&mut self.stream).await?;
|
||||
let count = read_u16(&mut self.stream).await?;
|
||||
for _ in 0..count {
|
||||
let _ = read_i32(&mut self.stream).await?;
|
||||
}
|
||||
Ok(RfbInputEvent::Ignored)
|
||||
}
|
||||
3 => {
|
||||
let mut buf = [0u8; 9];
|
||||
self.stream.read_exact(&mut buf).await?;
|
||||
Ok(RfbInputEvent::Ignored)
|
||||
}
|
||||
4 => {
|
||||
let down = read_u8(&mut self.stream).await? != 0;
|
||||
let mut pad = [0u8; 2];
|
||||
self.stream.read_exact(&mut pad).await?;
|
||||
let keysym = read_u32(&mut self.stream).await?;
|
||||
Ok(RfbInputEvent::Key(RfbKeyEvent { down, keysym }))
|
||||
}
|
||||
5 => {
|
||||
let button_mask = read_u8(&mut self.stream).await?;
|
||||
let x = read_u16(&mut self.stream).await?;
|
||||
let y = read_u16(&mut self.stream).await?;
|
||||
let previous_button_mask = self.last_buttons;
|
||||
self.last_buttons = button_mask;
|
||||
Ok(RfbInputEvent::Pointer(RfbPointerEvent {
|
||||
x,
|
||||
y,
|
||||
button_mask,
|
||||
previous_button_mask,
|
||||
}))
|
||||
}
|
||||
6 => {
|
||||
let mut hdr = [0u8; 7];
|
||||
self.stream.read_exact(&mut hdr).await?;
|
||||
let len = u32::from_be_bytes([hdr[3], hdr[4], hdr[5], hdr[6]]) as usize;
|
||||
let mut data = vec![0u8; len.min(1024 * 1024)];
|
||||
self.stream.read_exact(&mut data).await?;
|
||||
Ok(RfbInputEvent::Clipboard(
|
||||
String::from_utf8_lossy(&data).to_string(),
|
||||
))
|
||||
}
|
||||
_ => Err(AppError::BadRequest(format!(
|
||||
"Unsupported RFB message {}",
|
||||
msg_type
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_frame(&mut self, frame: RfbFrame) -> Result<()> {
|
||||
match frame {
|
||||
RfbFrame::Jpeg {
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
} => {
|
||||
self.maybe_resize(width, height).await?;
|
||||
self.write_frame_header(width, height, ENCODING_TIGHT)
|
||||
.await?;
|
||||
write_tight_jpeg_payload(&mut self.stream, &data).await?;
|
||||
}
|
||||
RfbFrame::H264 {
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
key,
|
||||
} => {
|
||||
self.maybe_resize(width, height).await?;
|
||||
if self.h264_waiting_keyframe && !key {
|
||||
return Ok(());
|
||||
}
|
||||
self.write_frame_header(width, height, ENCODING_H264)
|
||||
.await?;
|
||||
self.stream
|
||||
.write_all(&(data.len() as u32).to_be_bytes())
|
||||
.await?;
|
||||
self.stream
|
||||
.write_all(&(self.h264_waiting_keyframe as u32).to_be_bytes())
|
||||
.await?;
|
||||
self.stream.write_all(&data).await?;
|
||||
self.h264_waiting_keyframe = false;
|
||||
}
|
||||
}
|
||||
self.stream.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn maybe_resize(&mut self, width: u16, height: u16) -> Result<()> {
|
||||
if width == self.width && height == self.height {
|
||||
return Ok(());
|
||||
}
|
||||
if !self.encodings.has_resize {
|
||||
return Err(AppError::BadRequest(
|
||||
"VNC client does not support DesktopSize resize; reconnect required".to_string(),
|
||||
));
|
||||
}
|
||||
self.write_frame_header(width, height, ENCODING_DESKTOP_SIZE)
|
||||
.await?;
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
self.h264_waiting_keyframe = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_frame_header(&mut self, width: u16, height: u16, encoding: i32) -> Result<()> {
|
||||
self.stream.write_all(&[0, 0]).await?;
|
||||
self.stream.write_all(&1u16.to_be_bytes()).await?;
|
||||
self.stream.write_all(&0u16.to_be_bytes()).await?;
|
||||
self.stream.write_all(&0u16.to_be_bytes()).await?;
|
||||
self.stream.write_all(&width.to_be_bytes()).await?;
|
||||
self.stream.write_all(&height.to_be_bytes()).await?;
|
||||
self.stream.write_all(&encoding.to_be_bytes()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_tight_jpeg_payload(stream: &mut TcpStream, data: &[u8]) -> Result<()> {
|
||||
if data.len() > 0x3f_ffff {
|
||||
return Err(AppError::BadRequest(
|
||||
"JPEG frame too large for Tight encoding".to_string(),
|
||||
));
|
||||
}
|
||||
stream.write_all(&[0b1001_1111]).await?;
|
||||
write_compact_len(stream, data.len()).await?;
|
||||
stream.write_all(data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_compact_len(stream: &mut TcpStream, len: usize) -> Result<()> {
|
||||
if len <= 127 {
|
||||
stream.write_all(&[(len & 0x7f) as u8]).await?;
|
||||
} else if len <= 16_383 {
|
||||
stream
|
||||
.write_all(&[((len & 0x7f) as u8) | 0x80, ((len >> 7) & 0x7f) as u8])
|
||||
.await?;
|
||||
} else {
|
||||
stream
|
||||
.write_all(&[
|
||||
((len & 0x7f) as u8) | 0x80,
|
||||
(((len >> 7) & 0x7f) as u8) | 0x80,
|
||||
((len >> 14) & 0xff) as u8,
|
||||
])
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encrypt_vnc_challenge(challenge: &[u8; 16], password: &str) -> Result<[u8; 16]> {
|
||||
let mut key = [0u8; 8];
|
||||
for (dst, src) in key.iter_mut().zip(password.as_bytes().iter().take(8)) {
|
||||
*dst = reverse_bits(*src);
|
||||
}
|
||||
let cipher = des::Des::new_from_slice(&key)
|
||||
.map_err(|_| AppError::BadRequest("Invalid VNC DES key".to_string()))?;
|
||||
let mut out = *challenge;
|
||||
for chunk in out.chunks_exact_mut(8) {
|
||||
cipher.encrypt_block(chunk.into());
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn reverse_bits(byte: u8) -> u8 {
|
||||
byte.reverse_bits()
|
||||
}
|
||||
|
||||
async fn read_u8(stream: &mut TcpStream) -> Result<u8> {
|
||||
let mut buf = [0u8; 1];
|
||||
stream.read_exact(&mut buf).await?;
|
||||
Ok(buf[0])
|
||||
}
|
||||
|
||||
async fn read_u16(stream: &mut TcpStream) -> Result<u16> {
|
||||
let mut buf = [0u8; 2];
|
||||
stream.read_exact(&mut buf).await?;
|
||||
Ok(u16::from_be_bytes(buf))
|
||||
}
|
||||
|
||||
async fn read_u32(stream: &mut TcpStream) -> Result<u32> {
|
||||
let mut buf = [0u8; 4];
|
||||
stream.read_exact(&mut buf).await?;
|
||||
Ok(u32::from_be_bytes(buf))
|
||||
}
|
||||
|
||||
async fn read_i32(stream: &mut TcpStream) -> Result<i32> {
|
||||
let mut buf = [0u8; 4];
|
||||
stream.read_exact(&mut buf).await?;
|
||||
Ok(i32::from_be_bytes(buf))
|
||||
}
|
||||
|
||||
pub fn key_event_to_hid(event: RfbKeyEvent) -> Option<KeyboardEvent> {
|
||||
let key = keysym_to_key(event.keysym)?;
|
||||
Some(KeyboardEvent {
|
||||
event_type: if event.down {
|
||||
KeyEventType::Down
|
||||
} else {
|
||||
KeyEventType::Up
|
||||
},
|
||||
key,
|
||||
modifiers: KeyboardModifiers::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn keysym_to_key(keysym: u32) -> Option<CanonicalKey> {
|
||||
match keysym {
|
||||
0xff08 => Some(CanonicalKey::Backspace),
|
||||
0xff09 => Some(CanonicalKey::Tab),
|
||||
0xff0d => Some(CanonicalKey::Enter),
|
||||
0xff1b => Some(CanonicalKey::Escape),
|
||||
0xffff => Some(CanonicalKey::Delete),
|
||||
0xff50 => Some(CanonicalKey::Home),
|
||||
0xff51 => Some(CanonicalKey::ArrowLeft),
|
||||
0xff52 => Some(CanonicalKey::ArrowUp),
|
||||
0xff53 => Some(CanonicalKey::ArrowRight),
|
||||
0xff54 => Some(CanonicalKey::ArrowDown),
|
||||
0xff55 => Some(CanonicalKey::PageUp),
|
||||
0xff56 => Some(CanonicalKey::PageDown),
|
||||
0xff57 => Some(CanonicalKey::End),
|
||||
0xff63 => Some(CanonicalKey::Insert),
|
||||
0xffbe..=0xffc9 => CanonicalKey::from_hid_usage((keysym - 0xffbe + 0x3a) as u8),
|
||||
0x20 => Some(CanonicalKey::Space),
|
||||
0x61..=0x7a => CanonicalKey::from_hid_usage((keysym - 0x61 + 0x04) as u8),
|
||||
0x41..=0x5a => CanonicalKey::from_hid_usage((keysym - 0x41 + 0x04) as u8),
|
||||
0x31..=0x39 => CanonicalKey::from_hid_usage((keysym - 0x31 + 0x1e) as u8),
|
||||
0x30 => Some(CanonicalKey::Digit0),
|
||||
0x2d => Some(CanonicalKey::Minus),
|
||||
0x3d => Some(CanonicalKey::Equal),
|
||||
0x5b => Some(CanonicalKey::BracketLeft),
|
||||
0x5d => Some(CanonicalKey::BracketRight),
|
||||
0x5c => Some(CanonicalKey::Backslash),
|
||||
0x3b => Some(CanonicalKey::Semicolon),
|
||||
0x27 => Some(CanonicalKey::Quote),
|
||||
0x60 => Some(CanonicalKey::Backquote),
|
||||
0x2c => Some(CanonicalKey::Comma),
|
||||
0x2e => Some(CanonicalKey::Period),
|
||||
0x2f => Some(CanonicalKey::Slash),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pointer_event_to_hid(event: RfbPointerEvent, width: u16, height: u16) -> Vec<MouseEvent> {
|
||||
let mut out = Vec::new();
|
||||
let abs_x = ((event.x as u64 * 32767) / width.max(1) as u64) as i32;
|
||||
let abs_y = ((event.y as u64 * 32767) / height.max(1) as u64) as i32;
|
||||
out.push(MouseEvent {
|
||||
event_type: MouseEventType::MoveAbs,
|
||||
x: abs_x,
|
||||
y: abs_y,
|
||||
button: None,
|
||||
scroll: 0,
|
||||
});
|
||||
|
||||
if event.button_mask & 0x08 != 0 {
|
||||
out.push(MouseEvent::scroll(1));
|
||||
}
|
||||
if event.button_mask & 0x10 != 0 {
|
||||
out.push(MouseEvent::scroll(-1));
|
||||
}
|
||||
|
||||
for (bit, button) in [
|
||||
(0x01, MouseButton::Left),
|
||||
(0x02, MouseButton::Middle),
|
||||
(0x04, MouseButton::Right),
|
||||
] {
|
||||
if (event.button_mask ^ event.previous_button_mask) & bit == 0 {
|
||||
continue;
|
||||
}
|
||||
if event.button_mask & bit != 0 {
|
||||
out.push(MouseEvent::button_down(button));
|
||||
} else {
|
||||
out.push(MouseEvent::button_up(button));
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
@@ -6,7 +6,8 @@ use crate::rtsp::RtspService;
|
||||
use crate::state::AppState;
|
||||
use crate::stream_encoder::encoder_type_to_backend;
|
||||
use crate::video::codec_constraints::{
|
||||
enforce_constraints_with_stream_manager, StreamCodecConstraints,
|
||||
enforce_constraints_with_stream_manager, validate_third_party_codec_compatibility,
|
||||
StreamCodecConstraints,
|
||||
};
|
||||
use tokio::sync::{Mutex, OwnedMutexGuard};
|
||||
|
||||
@@ -409,6 +410,27 @@ pub async fn enforce_stream_codec_constraints(state: &Arc<AppState>) -> Result<O
|
||||
Ok(enforcement.message)
|
||||
}
|
||||
|
||||
fn validate_rustdesk_candidate(
|
||||
state: &Arc<AppState>,
|
||||
new_config: &crate::rustdesk::config::RustDeskConfig,
|
||||
) -> Result<()> {
|
||||
let mut candidate = state.config.get().as_ref().clone();
|
||||
candidate.rustdesk = new_config.clone();
|
||||
validate_third_party_codec_compatibility(&candidate)
|
||||
}
|
||||
|
||||
fn validate_vnc_candidate(state: &Arc<AppState>, new_config: &VncConfig) -> Result<()> {
|
||||
let mut candidate = state.config.get().as_ref().clone();
|
||||
candidate.vnc = new_config.clone();
|
||||
validate_third_party_codec_compatibility(&candidate)
|
||||
}
|
||||
|
||||
fn validate_rtsp_candidate(state: &Arc<AppState>, new_config: &RtspConfig) -> Result<()> {
|
||||
let mut candidate = state.config.get().as_ref().clone();
|
||||
candidate.rtsp = new_config.clone();
|
||||
validate_third_party_codec_compatibility(&candidate)
|
||||
}
|
||||
|
||||
pub async fn apply_rustdesk_config(
|
||||
state: &Arc<AppState>,
|
||||
old_config: &crate::rustdesk::config::RustDeskConfig,
|
||||
@@ -417,6 +439,8 @@ pub async fn apply_rustdesk_config(
|
||||
) -> Result<()> {
|
||||
tracing::info!("Applying RustDesk config changes...");
|
||||
|
||||
validate_rustdesk_candidate(state, new_config)?;
|
||||
|
||||
let mut rustdesk_guard = state.rustdesk.write().await;
|
||||
let mut credentials_to_save = None;
|
||||
|
||||
@@ -433,6 +457,7 @@ pub async fn apply_rustdesk_config(
|
||||
|
||||
if new_config.enabled {
|
||||
let need_restart = options.force
|
||||
|| old_config.codec != new_config.codec
|
||||
|| old_config.rendezvous_server != new_config.rendezvous_server
|
||||
|| old_config.device_id != new_config.device_id
|
||||
|| old_config.device_password != new_config.device_password;
|
||||
@@ -488,6 +513,77 @@ pub async fn apply_rustdesk_config(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn apply_vnc_config(
|
||||
state: &Arc<AppState>,
|
||||
old_config: &VncConfig,
|
||||
new_config: &VncConfig,
|
||||
options: ConfigApplyOptions,
|
||||
) -> Result<()> {
|
||||
tracing::info!("Applying VNC config changes...");
|
||||
|
||||
validate_vnc_candidate(state, new_config)?;
|
||||
|
||||
if new_config.enabled {
|
||||
let mut candidate = state.config.get().as_ref().clone();
|
||||
candidate.vnc = new_config.clone();
|
||||
let constraints = StreamCodecConstraints::from_config(&candidate);
|
||||
match enforce_constraints_with_stream_manager(&state.stream_manager, &constraints).await {
|
||||
Ok(result) if result.changed => {
|
||||
if let Some(message) = result.message {
|
||||
tracing::info!("{}", message);
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => tracing::warn!(
|
||||
"Failed to enforce VNC stream constraints before start: {}",
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
let mut vnc_guard = state.vnc.write().await;
|
||||
|
||||
if !new_config.enabled {
|
||||
if let Some(ref service) = *vnc_guard {
|
||||
service.stop().await?;
|
||||
}
|
||||
*vnc_guard = None;
|
||||
}
|
||||
|
||||
if new_config.enabled {
|
||||
let need_restart = options.force
|
||||
|| old_config.bind != new_config.bind
|
||||
|| old_config.port != new_config.port
|
||||
|| old_config.encoding != new_config.encoding
|
||||
|| old_config.password != new_config.password
|
||||
|| old_config.jpeg_quality != new_config.jpeg_quality
|
||||
|| old_config.allow_one_client != new_config.allow_one_client;
|
||||
|
||||
if vnc_guard.is_none() {
|
||||
let service = crate::vnc::VncService::new(
|
||||
new_config.clone(),
|
||||
state.stream_manager.clone(),
|
||||
state.hid.clone(),
|
||||
);
|
||||
service.start().await?;
|
||||
*vnc_guard = Some(Arc::new(service));
|
||||
tracing::info!("VNC service started");
|
||||
} else if need_restart {
|
||||
if let Some(ref service) = *vnc_guard {
|
||||
service.restart(new_config.clone()).await?;
|
||||
tracing::info!("VNC service restarted");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(vnc_guard);
|
||||
if let Some(message) = enforce_stream_codec_constraints(state).await? {
|
||||
tracing::info!("{}", message);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn apply_rtsp_config(
|
||||
state: &Arc<AppState>,
|
||||
old_config: &RtspConfig,
|
||||
@@ -496,6 +592,8 @@ pub async fn apply_rtsp_config(
|
||||
) -> Result<()> {
|
||||
tracing::info!("Applying RTSP config changes...");
|
||||
|
||||
validate_rtsp_candidate(state, new_config)?;
|
||||
|
||||
let mut rtsp_guard = state.rtsp.write().await;
|
||||
|
||||
if !new_config.enabled {
|
||||
|
||||
@@ -12,6 +12,7 @@ mod rtsp;
|
||||
mod rustdesk;
|
||||
mod stream;
|
||||
pub(crate) mod video;
|
||||
mod vnc;
|
||||
mod web;
|
||||
|
||||
pub use atx::{get_atx_config, update_atx_config};
|
||||
@@ -31,6 +32,9 @@ pub use rustdesk::{
|
||||
};
|
||||
pub use stream::{get_stream_config, update_stream_config};
|
||||
pub use video::{get_video_config, update_video_config};
|
||||
pub use vnc::{
|
||||
get_vnc_config, get_vnc_status, start_vnc_service, stop_vnc_service, update_vnc_config,
|
||||
};
|
||||
pub use web::{get_web_config, update_web_config};
|
||||
|
||||
use axum::{extract::State, Json};
|
||||
@@ -52,6 +56,7 @@ fn sanitize_config_for_api(config: &mut AppConfig) {
|
||||
config.rustdesk.signing_private_key = None;
|
||||
|
||||
config.rtsp.password = None;
|
||||
config.vnc.password = None;
|
||||
}
|
||||
|
||||
pub async fn get_all_config(State(state): State<Arc<AppState>>) -> Json<AppConfig> {
|
||||
|
||||
@@ -7,6 +7,44 @@ use crate::state::AppState;
|
||||
use super::apply::{apply_rtsp_config, try_apply_lock, ConfigApplyOptions};
|
||||
use super::types::{RtspConfigResponse, RtspConfigUpdate, RtspStatusResponse};
|
||||
|
||||
fn validate_candidate(state: &Arc<AppState>, config: &crate::config::RtspConfig) -> Result<()> {
|
||||
let mut candidate = state.config.get().as_ref().clone();
|
||||
candidate.rtsp = config.clone();
|
||||
crate::video::codec_constraints::validate_third_party_codec_compatibility(&candidate)
|
||||
}
|
||||
|
||||
async fn persist_and_apply(
|
||||
state: &Arc<AppState>,
|
||||
old_config: crate::config::RtspConfig,
|
||||
new_config: crate::config::RtspConfig,
|
||||
) -> Result<crate::config::RtspConfig> {
|
||||
validate_candidate(state, &new_config)?;
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
config.rtsp = new_config.clone();
|
||||
})
|
||||
.await?;
|
||||
let stored_config = state.config.get().rtsp.clone();
|
||||
apply_rtsp_config(
|
||||
state,
|
||||
&old_config,
|
||||
&stored_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
Ok(stored_config)
|
||||
}
|
||||
|
||||
async fn current_status(state: &Arc<AppState>) -> crate::rtsp::RtspServiceStatus {
|
||||
let guard = state.rtsp.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
service.status().await
|
||||
} else {
|
||||
crate::rtsp::RtspServiceStatus::Stopped
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_rtsp_config(State(state): State<Arc<AppState>>) -> Json<RtspConfigResponse> {
|
||||
let config = state.config.get();
|
||||
Json(RtspConfigResponse::from(&config.rtsp))
|
||||
@@ -14,14 +52,7 @@ pub async fn get_rtsp_config(State(state): State<Arc<AppState>>) -> Json<RtspCon
|
||||
|
||||
pub async fn get_rtsp_status(State(state): State<Arc<AppState>>) -> Json<RtspStatusResponse> {
|
||||
let config = state.config.get().rtsp.clone();
|
||||
let status = {
|
||||
let guard = state.rtsp.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
service.status().await
|
||||
} else {
|
||||
crate::rtsp::RtspServiceStatus::Stopped
|
||||
}
|
||||
};
|
||||
let status = current_status(&state).await;
|
||||
|
||||
Json(RtspStatusResponse::new(&config, status))
|
||||
}
|
||||
@@ -34,22 +65,9 @@ pub async fn update_rtsp_config(
|
||||
|
||||
let _apply_guard = try_apply_lock(&state.config_apply_locks.rtsp, "rtsp")?;
|
||||
let old_config = state.config.get().rtsp.clone();
|
||||
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
req.apply_to(&mut config.rtsp);
|
||||
})
|
||||
.await?;
|
||||
|
||||
let new_config = state.config.get().rtsp.clone();
|
||||
apply_rtsp_config(
|
||||
&state,
|
||||
&old_config,
|
||||
&new_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
let mut merged_config = old_config.clone();
|
||||
req.apply_to(&mut merged_config);
|
||||
let new_config = persist_and_apply(&state, old_config, merged_config).await?;
|
||||
|
||||
Ok(Json(RtspConfigResponse::from(&new_config)))
|
||||
}
|
||||
@@ -61,25 +79,10 @@ pub async fn start_rtsp_service(
|
||||
let current_config = state.config.get().rtsp.clone();
|
||||
let mut start_config = current_config.clone();
|
||||
start_config.enabled = true;
|
||||
let stored_config = persist_and_apply(&state, current_config, start_config).await?;
|
||||
let status = current_status(&state).await;
|
||||
|
||||
apply_rtsp_config(
|
||||
&state,
|
||||
¤t_config,
|
||||
&start_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let status = {
|
||||
let guard = state.rtsp.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
service.status().await
|
||||
} else {
|
||||
crate::rtsp::RtspServiceStatus::Stopped
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(RtspStatusResponse::new(¤t_config, status)))
|
||||
Ok(Json(RtspStatusResponse::new(&stored_config, status)))
|
||||
}
|
||||
|
||||
pub async fn stop_rtsp_service(
|
||||
@@ -90,22 +93,8 @@ pub async fn stop_rtsp_service(
|
||||
let mut stop_config = current_config.clone();
|
||||
stop_config.enabled = false;
|
||||
|
||||
apply_rtsp_config(
|
||||
&state,
|
||||
¤t_config,
|
||||
&stop_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
let stored_config = persist_and_apply(&state, current_config, stop_config).await?;
|
||||
let status = current_status(&state).await;
|
||||
|
||||
let status = {
|
||||
let guard = state.rtsp.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
service.status().await
|
||||
} else {
|
||||
crate::rtsp::RtspServiceStatus::Stopped
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(RtspStatusResponse::new(¤t_config, status)))
|
||||
Ok(Json(RtspStatusResponse::new(&stored_config, status)))
|
||||
}
|
||||
|
||||
@@ -8,9 +8,58 @@ use crate::state::AppState;
|
||||
use super::apply::{apply_rustdesk_config, try_apply_lock, ConfigApplyOptions};
|
||||
use super::types::RustDeskConfigUpdate;
|
||||
|
||||
fn validate_candidate(state: &Arc<AppState>, config: &RustDeskConfig) -> Result<()> {
|
||||
let mut candidate = state.config.get().as_ref().clone();
|
||||
candidate.rustdesk = config.clone();
|
||||
crate::video::codec_constraints::validate_third_party_codec_compatibility(&candidate)
|
||||
}
|
||||
|
||||
async fn persist_and_apply(
|
||||
state: &Arc<AppState>,
|
||||
old_config: RustDeskConfig,
|
||||
new_config: RustDeskConfig,
|
||||
) -> Result<RustDeskConfig> {
|
||||
validate_candidate(state, &new_config)?;
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
config.rustdesk = new_config.clone();
|
||||
})
|
||||
.await?;
|
||||
let stored_config = state.config.get().rustdesk.clone();
|
||||
apply_rustdesk_config(
|
||||
state,
|
||||
&old_config,
|
||||
&stored_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
Ok(stored_config)
|
||||
}
|
||||
|
||||
async fn current_status(state: &Arc<AppState>, config: RustDeskConfig) -> RustDeskStatusResponse {
|
||||
let (service_status, rendezvous_status) = {
|
||||
let guard = state.rustdesk.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
let status = format!("{}", service.status());
|
||||
let rv_status = service.rendezvous_status().map(|s| format!("{}", s));
|
||||
(status, rv_status)
|
||||
} else {
|
||||
("not_initialized".to_string(), None)
|
||||
}
|
||||
};
|
||||
|
||||
RustDeskStatusResponse {
|
||||
config: RustDeskConfigResponse::from(&config),
|
||||
service_status,
|
||||
rendezvous_status,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct RustDeskConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub codec: crate::rustdesk::config::RustDeskCodec,
|
||||
pub rendezvous_server: String,
|
||||
pub relay_server: Option<String>,
|
||||
pub device_id: String,
|
||||
@@ -23,6 +72,7 @@ impl From<&RustDeskConfig> for RustDeskConfigResponse {
|
||||
fn from(config: &RustDeskConfig) -> Self {
|
||||
Self {
|
||||
enabled: config.enabled,
|
||||
codec: config.codec,
|
||||
rendezvous_server: config.rendezvous_server.clone(),
|
||||
relay_server: config.relay_server.clone(),
|
||||
device_id: config.device_id.clone(),
|
||||
@@ -50,23 +100,7 @@ pub async fn get_rustdesk_status(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Json<RustDeskStatusResponse> {
|
||||
let config = state.config.get().rustdesk.clone();
|
||||
|
||||
let (service_status, rendezvous_status) = {
|
||||
let guard = state.rustdesk.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
let status = format!("{}", service.status());
|
||||
let rv_status = service.rendezvous_status().map(|s| format!("{}", s));
|
||||
(status, rv_status)
|
||||
} else {
|
||||
("not_initialized".to_string(), None)
|
||||
}
|
||||
};
|
||||
|
||||
Json(RustDeskStatusResponse {
|
||||
config: RustDeskConfigResponse::from(&config),
|
||||
service_status,
|
||||
rendezvous_status,
|
||||
})
|
||||
Json(current_status(&state, config).await)
|
||||
}
|
||||
|
||||
pub async fn update_rustdesk_config(
|
||||
@@ -81,22 +115,7 @@ pub async fn update_rustdesk_config(
|
||||
req.apply_to(&mut merged_config);
|
||||
req.validate_merged(&merged_config)?;
|
||||
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
config.rustdesk = merged_config.clone();
|
||||
})
|
||||
.await?;
|
||||
|
||||
let new_config = state.config.get().rustdesk.clone();
|
||||
|
||||
apply_rustdesk_config(
|
||||
&state,
|
||||
&old_config,
|
||||
&new_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
let new_config = persist_and_apply(&state, old_config, merged_config).await?;
|
||||
|
||||
let constraints = state.stream_manager.codec_constraints().await;
|
||||
if constraints.rustdesk_enabled || constraints.rtsp_enabled {
|
||||
@@ -152,31 +171,8 @@ pub async fn start_rustdesk_service(
|
||||
let current_config = state.config.get().rustdesk.clone();
|
||||
let mut start_config = current_config.clone();
|
||||
start_config.enabled = true;
|
||||
|
||||
apply_rustdesk_config(
|
||||
&state,
|
||||
¤t_config,
|
||||
&start_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (service_status, rendezvous_status) = {
|
||||
let guard = state.rustdesk.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
let status = format!("{}", service.status());
|
||||
let rv_status = service.rendezvous_status().map(|s| format!("{}", s));
|
||||
(status, rv_status)
|
||||
} else {
|
||||
("not_initialized".to_string(), None)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(RustDeskStatusResponse {
|
||||
config: RustDeskConfigResponse::from(¤t_config),
|
||||
service_status,
|
||||
rendezvous_status,
|
||||
}))
|
||||
let stored_config = persist_and_apply(&state, current_config, start_config).await?;
|
||||
Ok(Json(current_status(&state, stored_config).await))
|
||||
}
|
||||
|
||||
pub async fn stop_rustdesk_service(
|
||||
@@ -187,28 +183,6 @@ pub async fn stop_rustdesk_service(
|
||||
let mut stop_config = current_config.clone();
|
||||
stop_config.enabled = false;
|
||||
|
||||
apply_rustdesk_config(
|
||||
&state,
|
||||
¤t_config,
|
||||
&stop_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (service_status, rendezvous_status) = {
|
||||
let guard = state.rustdesk.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
let status = format!("{}", service.status());
|
||||
let rv_status = service.rendezvous_status().map(|s| format!("{}", s));
|
||||
(status, rv_status)
|
||||
} else {
|
||||
("not_initialized".to_string(), None)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(RustDeskStatusResponse {
|
||||
config: RustDeskConfigResponse::from(¤t_config),
|
||||
service_status,
|
||||
rendezvous_status,
|
||||
}))
|
||||
let stored_config = persist_and_apply(&state, current_config, stop_config).await?;
|
||||
Ok(Json(current_status(&state, stored_config).await))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::config::*;
|
||||
use crate::error::AppError;
|
||||
use crate::rtsp::RtspServiceStatus;
|
||||
use crate::rustdesk::config::RustDeskConfig;
|
||||
use crate::vnc::VncServiceStatus;
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(unix)]
|
||||
@@ -765,6 +766,7 @@ fn validate_rustdesk_relay_key(key: &str) -> Result<(), AppError> {
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RustDeskConfigUpdate {
|
||||
pub enabled: Option<bool>,
|
||||
pub codec: Option<crate::rustdesk::config::RustDeskCodec>,
|
||||
pub rendezvous_server: Option<String>,
|
||||
pub relay_server: Option<String>,
|
||||
pub relay_key: Option<String>,
|
||||
@@ -821,6 +823,9 @@ impl RustDeskConfigUpdate {
|
||||
if let Some(enabled) = self.enabled {
|
||||
config.enabled = enabled;
|
||||
}
|
||||
if let Some(codec) = self.codec {
|
||||
config.codec = codec;
|
||||
}
|
||||
if let Some(ref server) = self.rendezvous_server {
|
||||
config.rendezvous_server = server.clone();
|
||||
}
|
||||
@@ -904,6 +909,125 @@ pub struct RtspConfigUpdate {
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct VncConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub bind: String,
|
||||
pub port: u16,
|
||||
pub encoding: VncEncoding,
|
||||
pub jpeg_quality: u8,
|
||||
pub allow_one_client: bool,
|
||||
pub has_password: bool,
|
||||
}
|
||||
|
||||
impl From<&VncConfig> for VncConfigResponse {
|
||||
fn from(config: &VncConfig) -> Self {
|
||||
Self {
|
||||
enabled: config.enabled,
|
||||
bind: config.bind.clone(),
|
||||
port: config.port,
|
||||
encoding: config.encoding.clone(),
|
||||
jpeg_quality: config.jpeg_quality,
|
||||
allow_one_client: config.allow_one_client,
|
||||
has_password: config.password.as_deref().is_some_and(|p| !p.is_empty()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct VncStatusResponse {
|
||||
pub config: VncConfigResponse,
|
||||
pub service_status: String,
|
||||
pub connection_count: u32,
|
||||
}
|
||||
|
||||
impl VncStatusResponse {
|
||||
pub fn new(config: &VncConfig, status: VncServiceStatus, connection_count: usize) -> Self {
|
||||
Self {
|
||||
config: VncConfigResponse::from(config),
|
||||
service_status: status.to_string(),
|
||||
connection_count: connection_count as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct VncConfigUpdate {
|
||||
pub enabled: Option<bool>,
|
||||
pub bind: Option<String>,
|
||||
pub port: Option<u16>,
|
||||
pub encoding: Option<VncEncoding>,
|
||||
pub jpeg_quality: Option<u8>,
|
||||
pub allow_one_client: Option<bool>,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl VncConfigUpdate {
|
||||
pub fn validate(&self) -> crate::error::Result<()> {
|
||||
if let Some(port) = self.port {
|
||||
if port == 0 {
|
||||
return Err(AppError::BadRequest("VNC port cannot be 0".into()));
|
||||
}
|
||||
}
|
||||
if let Some(ref bind) = self.bind {
|
||||
if bind.parse::<std::net::IpAddr>().is_err() {
|
||||
return Err(AppError::BadRequest("VNC bind must be a valid IP".into()));
|
||||
}
|
||||
}
|
||||
if let Some(quality) = self.jpeg_quality {
|
||||
if !(10..=100).contains(&quality) {
|
||||
return Err(AppError::BadRequest(
|
||||
"VNC JPEG quality must be 10-100".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(ref password) = self.password {
|
||||
if !password.is_empty() && password.len() > 8 {
|
||||
return Err(AppError::BadRequest(
|
||||
"VNCAuth password must be at most 8 characters".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_merged(&self, config: &VncConfig) -> crate::error::Result<()> {
|
||||
if config.enabled && config.password.as_deref().unwrap_or("").is_empty() {
|
||||
return Err(AppError::BadRequest("VNC password is required".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, config: &mut VncConfig) {
|
||||
if let Some(enabled) = self.enabled {
|
||||
config.enabled = enabled;
|
||||
}
|
||||
if let Some(ref bind) = self.bind {
|
||||
config.bind = bind.clone();
|
||||
}
|
||||
if let Some(port) = self.port {
|
||||
config.port = port;
|
||||
}
|
||||
if let Some(ref encoding) = self.encoding {
|
||||
config.encoding = encoding.clone();
|
||||
}
|
||||
if let Some(quality) = self.jpeg_quality {
|
||||
config.jpeg_quality = quality;
|
||||
}
|
||||
if let Some(allow_one_client) = self.allow_one_client {
|
||||
config.allow_one_client = allow_one_client;
|
||||
}
|
||||
if let Some(ref password) = self.password {
|
||||
if !password.is_empty() {
|
||||
config.password = Some(password.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RtspConfigUpdate {
|
||||
pub fn validate(&self) -> crate::error::Result<()> {
|
||||
if let Some(port) = self.port {
|
||||
@@ -1188,6 +1312,7 @@ mod tests {
|
||||
fn rustdesk_relay_key_accepts_hbbs_style_base64_32_bytes() {
|
||||
let update = RustDeskConfigUpdate {
|
||||
enabled: None,
|
||||
codec: None,
|
||||
rendezvous_server: None,
|
||||
relay_server: None,
|
||||
relay_key: Some("pLU0pEj2IZnNVKzrIO1pIdwGA3dOVJJLkFIYGOCGH1E=".to_string()),
|
||||
@@ -1202,6 +1327,7 @@ mod tests {
|
||||
let not_32 = "AAAAAAAAAAAAAAAAAAAAAA==".to_string();
|
||||
let update = RustDeskConfigUpdate {
|
||||
enabled: None,
|
||||
codec: None,
|
||||
rendezvous_server: None,
|
||||
relay_server: None,
|
||||
relay_key: Some(not_32),
|
||||
|
||||
110
src/web/handlers/config/vnc.rs
Normal file
110
src/web/handlers/config/vnc.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use axum::{extract::State, Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::apply::{apply_vnc_config, try_apply_lock, ConfigApplyOptions};
|
||||
use super::types::{VncConfigResponse, VncConfigUpdate, VncStatusResponse};
|
||||
|
||||
fn validate_candidate(state: &Arc<AppState>, config: &crate::config::VncConfig) -> Result<()> {
|
||||
let mut candidate = state.config.get().as_ref().clone();
|
||||
candidate.vnc = config.clone();
|
||||
crate::video::codec_constraints::validate_third_party_codec_compatibility(&candidate)
|
||||
}
|
||||
|
||||
async fn persist_and_apply(
|
||||
state: &Arc<AppState>,
|
||||
old_config: crate::config::VncConfig,
|
||||
new_config: crate::config::VncConfig,
|
||||
) -> Result<crate::config::VncConfig> {
|
||||
validate_candidate(state, &new_config)?;
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
config.vnc = new_config.clone();
|
||||
})
|
||||
.await?;
|
||||
let stored_config = state.config.get().vnc.clone();
|
||||
apply_vnc_config(
|
||||
state,
|
||||
&old_config,
|
||||
&stored_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
)
|
||||
.await?;
|
||||
Ok(stored_config)
|
||||
}
|
||||
|
||||
async fn current_status(state: &Arc<AppState>) -> (crate::vnc::VncServiceStatus, usize) {
|
||||
let guard = state.vnc.read().await;
|
||||
if let Some(ref service) = *guard {
|
||||
(service.status().await, service.connection_count())
|
||||
} else {
|
||||
(crate::vnc::VncServiceStatus::Stopped, 0)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_vnc_config(State(state): State<Arc<AppState>>) -> Json<VncConfigResponse> {
|
||||
Json(VncConfigResponse::from(&state.config.get().vnc))
|
||||
}
|
||||
|
||||
pub async fn get_vnc_status(State(state): State<Arc<AppState>>) -> Json<VncStatusResponse> {
|
||||
let config = state.config.get().vnc.clone();
|
||||
let (status, connection_count) = current_status(&state).await;
|
||||
|
||||
Json(VncStatusResponse::new(&config, status, connection_count))
|
||||
}
|
||||
|
||||
pub async fn update_vnc_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<VncConfigUpdate>,
|
||||
) -> Result<Json<VncConfigResponse>> {
|
||||
req.validate()?;
|
||||
|
||||
let _apply_guard = try_apply_lock(&state.config_apply_locks.vnc, "vnc")?;
|
||||
let old_config = state.config.get().vnc.clone();
|
||||
let mut merged_config = old_config.clone();
|
||||
req.apply_to(&mut merged_config);
|
||||
req.validate_merged(&merged_config)?;
|
||||
let new_config = persist_and_apply(&state, old_config, merged_config).await?;
|
||||
|
||||
Ok(Json(VncConfigResponse::from(&new_config)))
|
||||
}
|
||||
|
||||
pub async fn start_vnc_service(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<VncStatusResponse>> {
|
||||
let _apply_guard = try_apply_lock(&state.config_apply_locks.vnc, "vnc")?;
|
||||
let current_config = state.config.get().vnc.clone();
|
||||
let mut start_config = current_config.clone();
|
||||
start_config.enabled = true;
|
||||
if start_config.password.as_deref().unwrap_or("").is_empty() {
|
||||
start_config.password = current_config.password.clone();
|
||||
}
|
||||
let stored_config = persist_and_apply(&state, current_config, start_config).await?;
|
||||
let (status, connection_count) = current_status(&state).await;
|
||||
|
||||
Ok(Json(VncStatusResponse::new(
|
||||
&stored_config,
|
||||
status,
|
||||
connection_count,
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn stop_vnc_service(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<VncStatusResponse>> {
|
||||
let _apply_guard = try_apply_lock(&state.config_apply_locks.vnc, "vnc")?;
|
||||
let current_config = state.config.get().vnc.clone();
|
||||
let mut stop_config = current_config.clone();
|
||||
stop_config.enabled = false;
|
||||
|
||||
let stored_config = persist_and_apply(&state, current_config, stop_config).await?;
|
||||
|
||||
Ok(Json(VncStatusResponse::new(
|
||||
&stored_config,
|
||||
crate::vnc::VncServiceStatus::Stopped,
|
||||
0,
|
||||
)))
|
||||
}
|
||||
@@ -241,6 +241,7 @@ pub struct StreamConstraintsResponse {
|
||||
pub struct ConstraintSources {
|
||||
pub rustdesk: bool,
|
||||
pub rtsp: bool,
|
||||
pub vnc: bool,
|
||||
}
|
||||
|
||||
/// Get stream codec constraints derived from enabled services.
|
||||
@@ -267,6 +268,7 @@ pub async fn stream_constraints_get(
|
||||
sources: ConstraintSources {
|
||||
rustdesk: constraints.rustdesk_enabled,
|
||||
rtsp: constraints.rtsp_enabled,
|
||||
vnc: constraints.vnc_enabled,
|
||||
},
|
||||
reason: constraints.reason,
|
||||
current_mode,
|
||||
|
||||
@@ -36,6 +36,7 @@ pub struct Capabilities {
|
||||
pub atx: CapabilityInfo,
|
||||
pub audio: CapabilityInfo,
|
||||
pub rustdesk: CapabilityInfo,
|
||||
pub vnc: CapabilityInfo,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -106,6 +107,11 @@ pub async fn system_info(State(state): State<Arc<AppState>>) -> Json<SystemInfo>
|
||||
backend: platform.rustdesk.selected_backend.clone(),
|
||||
reason: platform.rustdesk.reason.clone(),
|
||||
},
|
||||
vnc: CapabilityInfo {
|
||||
available: config.vnc.enabled && platform.vnc.available,
|
||||
backend: platform.vnc.selected_backend.clone(),
|
||||
reason: platform.vnc.reason.clone(),
|
||||
},
|
||||
},
|
||||
disk_space,
|
||||
device_info,
|
||||
|
||||
@@ -143,6 +143,15 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
||||
"/config/rustdesk/stop",
|
||||
post(handlers::config::stop_rustdesk_service),
|
||||
)
|
||||
// VNC configuration endpoints
|
||||
.route("/config/vnc", get(handlers::config::get_vnc_config))
|
||||
.route("/config/vnc", patch(handlers::config::update_vnc_config))
|
||||
.route("/config/vnc/status", get(handlers::config::get_vnc_status))
|
||||
.route(
|
||||
"/config/vnc/start",
|
||||
post(handlers::config::start_vnc_service),
|
||||
)
|
||||
.route("/config/vnc/stop", post(handlers::config::stop_vnc_service))
|
||||
// RTSP configuration endpoints
|
||||
.route("/config/rtsp", get(handlers::config::get_rtsp_config))
|
||||
.route("/config/rtsp", patch(handlers::config::update_rtsp_config))
|
||||
|
||||
Reference in New Issue
Block a user