feat: 新增 MJPEG/H.264 VNC 初步支持

This commit is contained in:
mofeng-git
2026-06-15 22:23:27 +08:00
parent 5c98aea7e3
commit c101ef1c80
34 changed files with 2270 additions and 354 deletions

View File

@@ -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,
}

View File

@@ -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)]

View File

@@ -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;

View File

@@ -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);

View File

@@ -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"]),

View File

@@ -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,

View File

@@ -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"]),

View File

@@ -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"]),

View File

@@ -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);

View File

@@ -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,

View File

@@ -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={}",

View File

@@ -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,

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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
View 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
View 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
}

View File

@@ -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 {

View File

@@ -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> {

View File

@@ -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,
&current_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(&current_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,
&current_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(&current_config, status)))
Ok(Json(RtspStatusResponse::new(&stored_config, status)))
}

View File

@@ -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,
&current_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(&current_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,
&current_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(&current_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))
}

View File

@@ -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),

View 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,
)))
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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))