feat: 增强 VNC 功能的兼容性

This commit is contained in:
mofeng-git
2026-07-14 22:59:33 +08:00
parent 1ded7a8a66
commit 6693020e6d
9 changed files with 1359 additions and 300 deletions

View File

@@ -41,7 +41,6 @@ pub struct VncConfig {
pub bind: String, pub bind: String,
pub port: u16, pub port: u16,
pub encoding: VncEncoding, pub encoding: VncEncoding,
pub jpeg_quality: u8,
pub allow_one_client: bool, pub allow_one_client: bool,
#[typeshare(skip)] #[typeshare(skip)]
pub password: Option<String>, pub password: Option<String>,
@@ -54,7 +53,6 @@ impl Default for VncConfig {
bind: "0.0.0.0".to_string(), bind: "0.0.0.0".to_string(),
port: 5900, port: 5900,
encoding: VncEncoding::TightJpeg, encoding: VncEncoding::TightJpeg,
jpeg_quality: 80,
allow_one_client: true, allow_one_client: true,
password: None, password: None,
} }

View File

@@ -9,7 +9,7 @@ use std::time::Duration;
use bytes::Bytes; use bytes::Bytes;
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{broadcast, Mutex, RwLock}; use tokio::sync::{broadcast, watch, Mutex, RwLock};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::{info, warn}; use tracing::{info, warn};
@@ -21,7 +21,19 @@ use crate::utils::{bind_socket_addr, bind_tcp_listener};
use crate::video::codec::{BitratePreset, VideoCodecType}; use crate::video::codec::{BitratePreset, VideoCodecType};
use crate::video::stream_manager::VideoStreamManager; use crate::video::stream_manager::VideoStreamManager;
use self::rfb::{RfbClient, RfbFrame, RfbInputEvent}; use self::rfb::{FrameSendOutcome, RfbClient, RfbFrame, RfbInputEvent};
struct ActiveClientGuard(Arc<AtomicUsize>);
impl Drop for ActiveClientGuard {
fn drop(&mut self) {
let _ = self
.0
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |count| {
Some(count.saturating_sub(1))
});
}
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum VncServiceStatus { pub enum VncServiceStatus {
@@ -145,7 +157,15 @@ impl VncService {
match result { match result {
Ok((stream, peer)) => { Ok((stream, peer)) => {
let cfg = config_ref.read().await.clone(); let cfg = config_ref.read().await.clone();
if cfg.allow_one_client && active_clients.load(Ordering::Relaxed) > 0 { let reserved = if cfg.allow_one_client {
active_clients
.compare_exchange(0, 1, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
} else {
active_clients.fetch_add(1, Ordering::AcqRel);
true
};
if !reserved {
warn!("Rejecting VNC client {} because another client is active", peer); warn!("Rejecting VNC client {} because another client is active", peer);
drop(stream); drop(stream);
continue; continue;
@@ -154,9 +174,8 @@ impl VncService {
let hid = hid.clone(); let hid = hid.clone();
let active = active_clients.clone(); let active = active_clients.clone();
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
active.fetch_add(1, Ordering::Relaxed); let _active_guard = ActiveClientGuard(active);
let result = handle_client(stream, peer, cfg, vm, hid).await; let result = handle_client(stream, peer, cfg, vm, hid).await;
active.fetch_sub(1, Ordering::Relaxed);
if let Err(err) = result { if let Err(err) = result {
warn!("VNC client {} ended: {}", peer, err); warn!("VNC client {} ended: {}", peer, err);
} }
@@ -234,24 +253,81 @@ async fn handle_client(
let (width, height) = initial_frame_size(&config, &video_manager).await; let (width, height) = initial_frame_size(&config, &video_manager).await;
client.set_size(width, height); client.set_size(width, height);
client.handshake().await?; client.handshake().await?;
tracing::debug!("VNC client {} ClientInit shared={}", peer, client.shared());
let (_, _, mut frame_rx) = subscribe_frames(&config, &video_manager).await?; let (_, _, mut frame_rx) = subscribe_frames(&config, &video_manager).await?;
let mut latest_frame = frame_rx.borrow().clone();
let mut latest_size = latest_frame.as_ref().map(RfbFrame::size);
let mut shutdown = client.shutdown_receiver(); let mut shutdown = client.shutdown_receiver();
loop { loop {
tokio::select! { tokio::select! {
biased;
result = client.read_input_event() => { result = client.read_input_event() => {
match result? { match result? {
RfbInputEvent::Ignored => {}
RfbInputEvent::Disconnected => break, RfbInputEvent::Disconnected => break,
event => handle_input_event(event, &hid, width, height).await?, RfbInputEvent::Key(key) => {
if let Some(event) = client.key_event_to_hid(key) {
hid.send_keyboard(event).await?;
} }
} }
maybe_frame = frame_rx.recv() => { RfbInputEvent::Pointer(pointer) => {
let Some(frame) = maybe_frame else { break }; let (width, height) = client.framebuffer_size();
client.send_frame(frame).await?; for event in rfb::pointer_event_to_hid(pointer, width, height) {
hid.send_mouse(event).await?;
}
}
RfbInputEvent::SetEncodings { encoding_enabled, resumed } => {
if !encoding_enabled {
tracing::debug!("VNC client {} paused the configured encoding", peer);
}
if resumed && config.encoding == VncEncoding::H264 {
request_vnc_keyframe(&video_manager, "encoding resume").await;
}
}
RfbInputEvent::FramebufferUpdateRequest(request) => {
if !request.incremental && config.encoding == VncEncoding::H264 {
request_vnc_keyframe(&video_manager, "non-incremental refresh").await;
}
}
RfbInputEvent::SetPixelFormat(format) => {
tracing::debug!(
"VNC client {} selected {} bpp true-colour={}",
peer,
format.bits_per_pixel,
format.true_colour
);
}
RfbInputEvent::UnsupportedClientCutText => {
tracing::debug!("Ignoring unsupported VNC ClientCutText from {}", peer);
}
}
}
changed = frame_rx.changed() => {
if changed.is_err() {
break;
}
latest_frame = frame_rx.borrow_and_update().clone();
let new_size = latest_frame.as_ref().map(RfbFrame::size);
if config.encoding == VncEncoding::H264
&& latest_size.is_some()
&& new_size != latest_size
{
request_vnc_keyframe(&video_manager, "source resolution change").await;
}
latest_size = new_size;
} }
_ = shutdown.recv() => break, _ = shutdown.recv() => break,
} }
if client.has_pending_request()
&& latest_frame.is_some()
&& !client.has_complete_buffered_input()?
&& send_latest_frame(&mut client, latest_frame.as_ref()).await?
== FrameSendOutcome::DesktopSizeSent
&& config.encoding == VncEncoding::H264
{
request_vnc_keyframe(&video_manager, "framebuffer resize").await;
}
} }
Ok(()) Ok(())
} }
@@ -276,8 +352,7 @@ async fn initial_frame_size(
async fn subscribe_frames( async fn subscribe_frames(
config: &VncConfig, config: &VncConfig,
video_manager: &Arc<VideoStreamManager>, video_manager: &Arc<VideoStreamManager>,
) -> Result<(u16, u16, tokio::sync::mpsc::Receiver<RfbFrame>)> { ) -> Result<(u16, u16, watch::Receiver<Option<RfbFrame>>)> {
let (tx, rx) = tokio::sync::mpsc::channel(4);
match config.encoding { match config.encoding {
VncEncoding::TightJpeg => { VncEncoding::TightJpeg => {
let handler = video_manager.mjpeg_handler(); let handler = video_manager.mjpeg_handler();
@@ -289,6 +364,15 @@ async fn subscribe_frames(
.as_ref() .as_ref()
.map(|f| (f.width() as u16, f.height() as u16)) .map(|f| (f.width() as u16, f.height() as u16))
.unwrap_or((800, 600)); .unwrap_or((800, 600));
let initial = current
.filter(|frame| frame.online && frame.is_valid_jpeg())
.map(|frame| RfbFrame::Jpeg {
data: frame.data_bytes(),
width: frame.width() as u16,
height: frame.height() as u16,
sequence: frame.sequence,
});
let (tx, rx) = watch::channel(initial);
let mut notify = handler.subscribe(); let mut notify = handler.subscribe();
tokio::spawn(async move { tokio::spawn(async move {
let _guard = guard; let _guard = guard;
@@ -302,19 +386,22 @@ async fn subscribe_frames(
if !frame.online || !frame.is_valid_jpeg() { if !frame.online || !frame.is_valid_jpeg() {
continue; continue;
} }
let _ = tx if tx.receiver_count() == 0 {
.send(RfbFrame::Jpeg { break;
}
tx.send_replace(Some(RfbFrame::Jpeg {
data: frame.data_bytes(), data: frame.data_bytes(),
width: frame.width() as u16, width: frame.width() as u16,
height: frame.height() as u16, height: frame.height() as u16,
}) sequence: frame.sequence,
.await; }));
handler.record_frame_sent(&client_id); handler.record_frame_sent(&client_id);
} }
}); });
Ok((width, height, rx)) Ok((width, height, rx))
} }
VncEncoding::H264 => { VncEncoding::H264 => {
let (tx, rx) = watch::channel(None);
video_manager.set_video_codec(VideoCodecType::H264).await?; video_manager.set_video_codec(VideoCodecType::H264).await?;
let mut frames = video_manager let mut frames = video_manager
.subscribe_encoded_frames() .subscribe_encoded_frames()
@@ -329,22 +416,28 @@ async fn subscribe_frames(
.unwrap_or(crate::video::format::Resolution::HD720); .unwrap_or(crate::video::format::Resolution::HD720);
let width = geometry.width as u16; let width = geometry.width as u16;
let height = geometry.height as u16; let height = geometry.height as u16;
if let Err(err) = video_manager.request_keyframe().await { request_vnc_keyframe(video_manager, "initial frame").await;
warn!("Failed to request VNC H264 keyframe: {}", err); let geometry_manager = video_manager.clone();
}
tokio::spawn(async move { tokio::spawn(async move {
while let Some(frame) = frames.recv().await { while let Some(frame) = frames.recv().await {
if frame.codec != crate::video::codec::registry::VideoEncoderType::H264 { if frame.codec != crate::video::codec::registry::VideoEncoderType::H264 {
continue; continue;
} }
let _ = tx if tx.receiver_count() == 0 {
.send(RfbFrame::H264 { break;
}
let geometry = geometry_manager
.get_encoding_config()
.await
.map(|cfg| cfg.resolution)
.unwrap_or(crate::video::format::Resolution::HD720);
tx.send_replace(Some(RfbFrame::H264 {
data: Bytes::copy_from_slice(&frame.data), data: Bytes::copy_from_slice(&frame.data),
width, width: geometry.width as u16,
height, height: geometry.height as u16,
key: frame.is_keyframe, key: frame.is_keyframe,
}) sequence: frame.sequence,
.await; }));
} }
}); });
Ok((width, height, rx)) Ok((width, height, rx))
@@ -352,25 +445,21 @@ async fn subscribe_frames(
} }
} }
async fn handle_input_event( async fn send_latest_frame(
event: RfbInputEvent, client: &mut RfbClient,
hid: &Arc<HidController>, frame: Option<&RfbFrame>,
width: u16, ) -> Result<FrameSendOutcome> {
height: u16, match frame {
) -> Result<()> { Some(frame) => client.send_frame(frame).await,
match event { None => Ok(FrameSendOutcome::NotSent),
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) { async fn request_vnc_keyframe(video_manager: &VideoStreamManager, reason: &str) {
hid.send_mouse(event).await?; if let Err(err) = video_manager.request_keyframe().await {
warn!(
"Failed to request VNC H264 keyframe for {}: {}",
reason, err
);
} }
} }
RfbInputEvent::Clipboard(_) => {}
RfbInputEvent::Ignored | RfbInputEvent::Disconnected => {}
}
Ok(())
}

File diff suppressed because it is too large Load Diff

View File

@@ -556,7 +556,6 @@ pub async fn apply_vnc_config(
|| old_config.port != new_config.port || old_config.port != new_config.port
|| old_config.encoding != new_config.encoding || old_config.encoding != new_config.encoding
|| old_config.password != new_config.password || old_config.password != new_config.password
|| old_config.jpeg_quality != new_config.jpeg_quality
|| old_config.allow_one_client != new_config.allow_one_client; || old_config.allow_one_client != new_config.allow_one_client;
if vnc_guard.is_none() { if vnc_guard.is_none() {

View File

@@ -941,7 +941,6 @@ pub struct VncConfigResponse {
pub bind: String, pub bind: String,
pub port: u16, pub port: u16,
pub encoding: VncEncoding, pub encoding: VncEncoding,
pub jpeg_quality: u8,
pub allow_one_client: bool, pub allow_one_client: bool,
pub has_password: bool, pub has_password: bool,
} }
@@ -953,7 +952,6 @@ impl From<&VncConfig> for VncConfigResponse {
bind: config.bind.clone(), bind: config.bind.clone(),
port: config.port, port: config.port,
encoding: config.encoding.clone(), encoding: config.encoding.clone(),
jpeg_quality: config.jpeg_quality,
allow_one_client: config.allow_one_client, allow_one_client: config.allow_one_client,
has_password: config.password.as_deref().is_some_and(|p| !p.is_empty()), has_password: config.password.as_deref().is_some_and(|p| !p.is_empty()),
} }
@@ -985,7 +983,6 @@ pub struct VncConfigUpdate {
pub bind: Option<String>, pub bind: Option<String>,
pub port: Option<u16>, pub port: Option<u16>,
pub encoding: Option<VncEncoding>, pub encoding: Option<VncEncoding>,
pub jpeg_quality: Option<u8>,
pub allow_one_client: Option<bool>, pub allow_one_client: Option<bool>,
pub password: Option<String>, pub password: Option<String>,
} }
@@ -1002,13 +999,6 @@ impl VncConfigUpdate {
return Err(AppError::BadRequest("VNC bind must be a valid IP".into())); 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 let Some(ref password) = self.password {
if !password.is_empty() && password.len() > 8 { if !password.is_empty() && password.len() > 8 {
return Err(AppError::BadRequest( return Err(AppError::BadRequest(
@@ -1039,9 +1029,6 @@ impl VncConfigUpdate {
if let Some(ref encoding) = self.encoding { if let Some(ref encoding) = self.encoding {
config.encoding = encoding.clone(); 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 { if let Some(allow_one_client) = self.allow_one_client {
config.allow_one_client = allow_one_client; config.allow_one_client = allow_one_client;
} }
@@ -1403,7 +1390,6 @@ mod tests {
bind: Some(bind.to_string()), bind: Some(bind.to_string()),
port: Some(5900), port: Some(5900),
encoding: None, encoding: None,
jpeg_quality: None,
allow_one_client: None, allow_one_client: None,
password: None, password: None,
}; };
@@ -1422,7 +1408,6 @@ mod tests {
bind: Some(bind.to_string()), bind: Some(bind.to_string()),
port: Some(5900), port: Some(5900),
encoding: None, encoding: None,
jpeg_quality: None,
allow_one_client: None, allow_one_client: None,
password: None, password: None,
}; };
@@ -1472,4 +1457,27 @@ mod tests {
); );
} }
} }
#[test]
fn legacy_vnc_jpeg_quality_is_ignored_and_not_returned() {
let config: VncConfig = serde_json::from_value(serde_json::json!({
"enabled": false,
"bind": "0.0.0.0",
"port": 5900,
"encoding": "tight_jpeg",
"jpeg_quality": 37,
"allow_one_client": true
}))
.expect("legacy VNC config should deserialize");
let update: VncConfigUpdate = serde_json::from_value(serde_json::json!({
"jpeg_quality": 37,
"allow_one_client": false
}))
.expect("legacy VNC update should deserialize");
assert_eq!(update.allow_one_client, Some(false));
let response = serde_json::to_value(VncConfigResponse::from(&config))
.expect("VNC response should serialize");
assert!(response.get("jpeg_quality").is_none());
}
} }

View File

@@ -1085,7 +1085,6 @@ export default {
encodingTightJpeg: 'Tight JPEG', encodingTightJpeg: 'Tight JPEG',
encodingH264: 'H.264', encodingH264: 'H.264',
encodingHint: 'VNC locks output while running. VNC cannot start under an H.265 lock; MJPEG blocks RTSP and RustDesk.', encodingHint: 'VNC locks output while running. VNC cannot start under an H.265 lock; MJPEG blocks RTSP and RustDesk.',
jpegQuality: 'JPEG Quality',
allowOneClient: 'Allow One Client Only', allowOneClient: 'Allow One Client Only',
password: 'Password', password: 'Password',
passwordPlaceholder: 'Leave empty to keep current', passwordPlaceholder: 'Leave empty to keep current',

View File

@@ -1084,7 +1084,6 @@ export default {
encodingTightJpeg: 'Tight JPEG', encodingTightJpeg: 'Tight JPEG',
encodingH264: 'H.264', encodingH264: 'H.264',
encodingHint: 'VNC 运行时会锁定编码H.265 锁定时 VNC 无法启动MJPEG 锁定时 RTSP 与 RustDesk 无法启动。', encodingHint: 'VNC 运行时会锁定编码H.265 锁定时 VNC 无法启动MJPEG 锁定时 RTSP 与 RustDesk 无法启动。',
jpegQuality: 'JPEG 质量',
allowOneClient: '仅允许单客户端', allowOneClient: '仅允许单客户端',
password: '密码', password: '密码',
passwordPlaceholder: '留空表示不修改', passwordPlaceholder: '留空表示不修改',

View File

@@ -259,7 +259,6 @@ export interface VncConfig {
bind: string; bind: string;
port: number; port: number;
encoding: VncEncoding; encoding: VncEncoding;
jpeg_quality: number;
allow_one_client: boolean; allow_one_client: boolean;
} }
@@ -639,7 +638,6 @@ export interface VncConfigResponse {
bind: string; bind: string;
port: number; port: number;
encoding: VncEncoding; encoding: VncEncoding;
jpeg_quality: number;
allow_one_client: boolean; allow_one_client: boolean;
has_password: boolean; has_password: boolean;
} }
@@ -649,7 +647,6 @@ export interface VncConfigUpdate {
bind?: string; bind?: string;
port?: number; port?: number;
encoding?: VncEncoding; encoding?: VncEncoding;
jpeg_quality?: number;
allow_one_client?: boolean; allow_one_client?: boolean;
password?: string; password?: string;
} }

View File

@@ -424,7 +424,6 @@ const vncLocalConfig = ref<VncConfigUpdate & { password?: string }>({
bind: '0.0.0.0', bind: '0.0.0.0',
port: 5900, port: 5900,
encoding: 'tight_jpeg', encoding: 'tight_jpeg',
jpeg_quality: 80,
allow_one_client: true, allow_one_client: true,
password: '', password: '',
}) })
@@ -2496,7 +2495,6 @@ function applyVncStatus(status: VncStatusResponse) {
bind: status.config.bind, bind: status.config.bind,
port: status.config.port, port: status.config.port,
encoding: status.config.encoding, encoding: status.config.encoding,
jpeg_quality: status.config.jpeg_quality,
allow_one_client: status.config.allow_one_client, allow_one_client: status.config.allow_one_client,
password: '', password: '',
} }
@@ -2519,7 +2517,6 @@ function vncUpdatePayload(enabled = !!vncLocalConfig.value.enabled): VncConfigUp
bind: vncLocalConfig.value.bind?.trim() || '0.0.0.0', bind: vncLocalConfig.value.bind?.trim() || '0.0.0.0',
port: Number(vncLocalConfig.value.port) || 5900, port: Number(vncLocalConfig.value.port) || 5900,
encoding: vncLocalConfig.value.encoding || 'tight_jpeg', encoding: vncLocalConfig.value.encoding || 'tight_jpeg',
jpeg_quality: Number(vncLocalConfig.value.jpeg_quality) || 80,
allow_one_client: !!vncLocalConfig.value.allow_one_client, allow_one_client: !!vncLocalConfig.value.allow_one_client,
} }
const password = (vncLocalConfig.value.password || '').trim() const password = (vncLocalConfig.value.password || '').trim()
@@ -4844,10 +4841,6 @@ watch(isWindows, () => {
<p class="text-xs text-muted-foreground">{{ t('extensions.vnc.encodingHint') }}</p> <p class="text-xs text-muted-foreground">{{ t('extensions.vnc.encodingHint') }}</p>
</div> </div>
</div> </div>
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
<Label class="sm:text-right">{{ t('extensions.vnc.jpegQuality') }}</Label>
<Input v-model.number="vncLocalConfig.jpeg_quality" class="sm:col-span-3" type="number" min="10" max="100" :disabled="vncStatus?.service_status === 'running' || vncLocalConfig.encoding !== 'tight_jpeg'" />
</div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<Label>{{ t('extensions.vnc.allowOneClient') }}</Label> <Label>{{ t('extensions.vnc.allowOneClient') }}</Label>
<Switch v-model="vncLocalConfig.allow_one_client" :disabled="vncStatus?.service_status === 'running'" /> <Switch v-model="vncLocalConfig.allow_one_client" :disabled="vncStatus?.service_status === 'running'" />