mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-30 01:41:45 +08:00
feat: 增强 VNC 功能的兼容性
This commit is contained in:
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
179
src/vnc/mod.rs
179
src/vnc/mod.rs
@@ -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?;
|
|
||||||
|
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::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(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
1409
src/vnc/rfb.rs
1409
src/vnc/rfb.rs
File diff suppressed because it is too large
Load Diff
@@ -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() {
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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: '留空表示不修改',
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'" />
|
||||||
|
|||||||
Reference in New Issue
Block a user