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

View File

@@ -9,7 +9,7 @@ use std::time::Duration;
use bytes::Bytes;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{broadcast, Mutex, RwLock};
use tokio::sync::{broadcast, watch, Mutex, RwLock};
use tokio::task::JoinHandle;
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::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)]
pub enum VncServiceStatus {
@@ -145,7 +157,15 @@ impl VncService {
match result {
Ok((stream, peer)) => {
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);
drop(stream);
continue;
@@ -154,9 +174,8 @@ impl VncService {
let hid = hid.clone();
let active = active_clients.clone();
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;
active.fetch_sub(1, Ordering::Relaxed);
if let Err(err) = result {
warn!("VNC client {} ended: {}", peer, err);
}
@@ -234,24 +253,81 @@ async fn handle_client(
let (width, height) = initial_frame_size(&config, &video_manager).await;
client.set_size(width, height);
client.handshake().await?;
tracing::debug!("VNC client {} ClientInit shared={}", peer, client.shared());
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();
loop {
tokio::select! {
biased;
result = client.read_input_event() => {
match result? {
RfbInputEvent::Ignored => {}
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() => {
let Some(frame) = maybe_frame else { break };
client.send_frame(frame).await?;
RfbInputEvent::Pointer(pointer) => {
let (width, height) = client.framebuffer_size();
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,
}
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(())
}
@@ -276,8 +352,7 @@ async fn initial_frame_size(
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);
) -> Result<(u16, u16, watch::Receiver<Option<RfbFrame>>)> {
match config.encoding {
VncEncoding::TightJpeg => {
let handler = video_manager.mjpeg_handler();
@@ -289,6 +364,15 @@ async fn subscribe_frames(
.as_ref()
.map(|f| (f.width() as u16, f.height() as u16))
.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();
tokio::spawn(async move {
let _guard = guard;
@@ -302,19 +386,22 @@ async fn subscribe_frames(
if !frame.online || !frame.is_valid_jpeg() {
continue;
}
let _ = tx
.send(RfbFrame::Jpeg {
if tx.receiver_count() == 0 {
break;
}
tx.send_replace(Some(RfbFrame::Jpeg {
data: frame.data_bytes(),
width: frame.width() as u16,
height: frame.height() as u16,
})
.await;
sequence: frame.sequence,
}));
handler.record_frame_sent(&client_id);
}
});
Ok((width, height, rx))
}
VncEncoding::H264 => {
let (tx, rx) = watch::channel(None);
video_manager.set_video_codec(VideoCodecType::H264).await?;
let mut frames = video_manager
.subscribe_encoded_frames()
@@ -329,22 +416,28 @@ async fn subscribe_frames(
.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);
}
request_vnc_keyframe(video_manager, "initial frame").await;
let geometry_manager = video_manager.clone();
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 {
if tx.receiver_count() == 0 {
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),
width,
height,
width: geometry.width as u16,
height: geometry.height as u16,
key: frame.is_keyframe,
})
.await;
sequence: frame.sequence,
}));
}
});
Ok((width, height, rx))
@@ -352,25 +445,21 @@ async fn subscribe_frames(
}
}
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?;
async fn send_latest_frame(
client: &mut RfbClient,
frame: Option<&RfbFrame>,
) -> Result<FrameSendOutcome> {
match frame {
Some(frame) => client.send_frame(frame).await,
None => Ok(FrameSendOutcome::NotSent),
}
}
RfbInputEvent::Pointer(pointer) => {
for event in rfb::pointer_event_to_hid(pointer, width, height) {
hid.send_mouse(event).await?;
async fn request_vnc_keyframe(video_manager: &VideoStreamManager, reason: &str) {
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.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() {

View File

@@ -941,7 +941,6 @@ pub struct VncConfigResponse {
pub bind: String,
pub port: u16,
pub encoding: VncEncoding,
pub jpeg_quality: u8,
pub allow_one_client: bool,
pub has_password: bool,
}
@@ -953,7 +952,6 @@ impl From<&VncConfig> for VncConfigResponse {
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()),
}
@@ -985,7 +983,6 @@ pub struct VncConfigUpdate {
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>,
}
@@ -1002,13 +999,6 @@ impl VncConfigUpdate {
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(
@@ -1039,9 +1029,6 @@ impl VncConfigUpdate {
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;
}
@@ -1403,7 +1390,6 @@ mod tests {
bind: Some(bind.to_string()),
port: Some(5900),
encoding: None,
jpeg_quality: None,
allow_one_client: None,
password: None,
};
@@ -1422,7 +1408,6 @@ mod tests {
bind: Some(bind.to_string()),
port: Some(5900),
encoding: None,
jpeg_quality: None,
allow_one_client: 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',
encodingH264: 'H.264',
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',
password: 'Password',
passwordPlaceholder: 'Leave empty to keep current',

View File

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

View File

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

View File

@@ -424,7 +424,6 @@ const vncLocalConfig = ref<VncConfigUpdate & { password?: string }>({
bind: '0.0.0.0',
port: 5900,
encoding: 'tight_jpeg',
jpeg_quality: 80,
allow_one_client: true,
password: '',
})
@@ -2496,7 +2495,6 @@ function applyVncStatus(status: VncStatusResponse) {
bind: status.config.bind,
port: status.config.port,
encoding: status.config.encoding,
jpeg_quality: status.config.jpeg_quality,
allow_one_client: status.config.allow_one_client,
password: '',
}
@@ -2519,7 +2517,6 @@ function vncUpdatePayload(enabled = !!vncLocalConfig.value.enabled): VncConfigUp
bind: vncLocalConfig.value.bind?.trim() || '0.0.0.0',
port: Number(vncLocalConfig.value.port) || 5900,
encoding: vncLocalConfig.value.encoding || 'tight_jpeg',
jpeg_quality: Number(vncLocalConfig.value.jpeg_quality) || 80,
allow_one_client: !!vncLocalConfig.value.allow_one_client,
}
const password = (vncLocalConfig.value.password || '').trim()
@@ -4844,10 +4841,6 @@ watch(isWindows, () => {
<p class="text-xs text-muted-foreground">{{ t('extensions.vnc.encodingHint') }}</p>
</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">
<Label>{{ t('extensions.vnc.allowOneClient') }}</Label>
<Switch v-model="vncLocalConfig.allow_one_client" :disabled="vncStatus?.service_status === 'running'" />