mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-19 02:11:50 +08:00
feat: 新增 MJPEG/H.264 VNC 初步支持
This commit is contained in:
@@ -171,6 +171,7 @@ export const extensionsApi = {
|
||||
|
||||
export interface RustDeskConfigResponse {
|
||||
enabled: boolean
|
||||
codec: 'h264' | 'h265'
|
||||
rendezvous_server: string
|
||||
relay_server: string | null
|
||||
device_id: string
|
||||
@@ -187,6 +188,7 @@ export interface RustDeskStatusResponse {
|
||||
|
||||
export interface RustDeskConfigUpdate {
|
||||
enabled?: boolean
|
||||
codec?: 'h264' | 'h265'
|
||||
rendezvous_server?: string
|
||||
relay_server?: string
|
||||
relay_key?: string
|
||||
@@ -271,6 +273,50 @@ export const rtspConfigApi = {
|
||||
stop: () => request<RtspStatusResponse>('/config/rtsp/stop', { method: 'POST' }),
|
||||
}
|
||||
|
||||
export type VncEncoding = 'tight_jpeg' | 'h264'
|
||||
|
||||
export interface VncConfigResponse {
|
||||
enabled: boolean
|
||||
bind: string
|
||||
port: number
|
||||
encoding: VncEncoding
|
||||
jpeg_quality: number
|
||||
allow_one_client: boolean
|
||||
has_password: boolean
|
||||
}
|
||||
|
||||
export interface VncConfigUpdate {
|
||||
enabled?: boolean
|
||||
bind?: string
|
||||
port?: number
|
||||
encoding?: VncEncoding
|
||||
jpeg_quality?: number
|
||||
allow_one_client?: boolean
|
||||
password?: string
|
||||
}
|
||||
|
||||
export interface VncStatusResponse {
|
||||
config: VncConfigResponse
|
||||
service_status: string
|
||||
connection_count: number
|
||||
}
|
||||
|
||||
export const vncConfigApi = {
|
||||
get: () => request<VncConfigResponse>('/config/vnc'),
|
||||
|
||||
update: (config: VncConfigUpdate) =>
|
||||
request<VncConfigResponse>('/config/vnc', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
|
||||
getStatus: () => request<VncStatusResponse>('/config/vnc/status'),
|
||||
|
||||
start: () => request<VncStatusResponse>('/config/vnc/start', { method: 'POST' }),
|
||||
|
||||
stop: () => request<VncStatusResponse>('/config/vnc/stop', { method: 'POST' }),
|
||||
}
|
||||
|
||||
export type WebConfig = WebConfigResponse
|
||||
|
||||
export type { WebConfigUpdate }
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface PlatformCapabilities {
|
||||
otg: FeatureCapability
|
||||
audio: FeatureCapability
|
||||
rustdesk: FeatureCapability
|
||||
vnc: FeatureCapability
|
||||
diagnostics: FeatureCapability
|
||||
extensions: FeatureCapability
|
||||
service_installation: FeatureCapability
|
||||
@@ -86,6 +87,7 @@ export const systemApi = {
|
||||
atx: { available: boolean; backend?: string; reason?: string }
|
||||
audio: { available: boolean; backend?: string; reason?: string }
|
||||
rustdesk: { available: boolean; backend?: string; reason?: string }
|
||||
vnc: { available: boolean; backend?: string; reason?: string }
|
||||
}
|
||||
disk_space?: {
|
||||
total: number
|
||||
@@ -206,6 +208,7 @@ export interface StreamConstraintsResponse {
|
||||
sources: {
|
||||
rustdesk: boolean
|
||||
rtsp: boolean
|
||||
vnc: boolean
|
||||
}
|
||||
reason: string
|
||||
current_mode: string
|
||||
@@ -719,6 +722,7 @@ export {
|
||||
redfishConfigApi,
|
||||
rustdeskConfigApi,
|
||||
rtspConfigApi,
|
||||
vncConfigApi,
|
||||
webConfigApi,
|
||||
type RustDeskConfigResponse,
|
||||
type RustDeskStatusResponse,
|
||||
@@ -729,6 +733,10 @@ export {
|
||||
type RedfishConfigUpdate,
|
||||
type RtspConfigUpdate,
|
||||
type RtspStatusResponse,
|
||||
type VncConfigResponse,
|
||||
type VncConfigUpdate,
|
||||
type VncEncoding,
|
||||
type VncStatusResponse,
|
||||
type WebConfig,
|
||||
type WebConfigUpdate,
|
||||
} from './config'
|
||||
|
||||
@@ -522,8 +522,7 @@ export default {
|
||||
environmentSubtitle: 'System runtime environment and USB device maintenance',
|
||||
aboutSubtitle: 'Online upgrade, version info and hardware overview',
|
||||
extTtydSubtitle: 'Open a host Shell terminal in the browser',
|
||||
extRustdeskSubtitle: 'Remote graphical access via RustDesk',
|
||||
extRtspSubtitle: 'Provide an RTSP video stream for external clients',
|
||||
thirdPartyAccessSubtitle: 'Configure external RustDesk, VNC, and RTSP access',
|
||||
extRemoteAccessSubtitle: 'Remote access through NAT-traversal services',
|
||||
extFrpcSubtitle: 'NAT traversal through the FRP client',
|
||||
aboutDesc: 'Open and Lightweight IP-KVM Solution',
|
||||
@@ -967,6 +966,10 @@ export default {
|
||||
start: 'Start',
|
||||
stop: 'Stop',
|
||||
autoStart: 'Auto Start',
|
||||
thirdPartyAccess: {
|
||||
title: 'Third-party Access',
|
||||
desc: 'Configure RustDesk, VNC, and RTSP in one place',
|
||||
},
|
||||
viewLogs: 'View Logs',
|
||||
noLogs: 'No logs available',
|
||||
binaryNotFound: '{path} not found, please install the required program',
|
||||
@@ -1040,6 +1043,8 @@ export default {
|
||||
relayServer: 'Relay Server',
|
||||
relayServerPlaceholder: 'hbbr.example.com:21117',
|
||||
relayKey: 'Relay Key',
|
||||
codec: 'Codec',
|
||||
codecHint: 'Choose H.264 or H.265 before starting RustDesk. The codec is locked while running.',
|
||||
deviceInfo: 'Device Info',
|
||||
deviceId: 'Device ID',
|
||||
deviceIdHint: 'Use this ID in RustDesk client to connect',
|
||||
@@ -1073,7 +1078,7 @@ export default {
|
||||
pathPlaceholder: 'live',
|
||||
pathHint: 'Example: rtsp://device-ip:8554/live',
|
||||
codec: 'Codec',
|
||||
codecHint: 'Enabling RTSP locks codec to selected value and disables MJPEG.',
|
||||
codecHint: 'RTSP locks output to the selected codec while running. If RustDesk is running, choose the same codec.',
|
||||
allowOneClient: 'Allow One Client Only',
|
||||
username: 'Username',
|
||||
usernamePlaceholder: 'Empty means no authentication',
|
||||
@@ -1081,6 +1086,26 @@ export default {
|
||||
passwordPlaceholder: 'Enter new password',
|
||||
urlPreview: 'RTSP URL Preview',
|
||||
},
|
||||
vnc: {
|
||||
title: 'VNC Remote',
|
||||
desc: 'Access via TigerVNC client',
|
||||
bind: 'Bind Address',
|
||||
port: 'Port',
|
||||
encoding: 'Video Encoding',
|
||||
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',
|
||||
passwordRequiredPlaceholder: 'Up to 8 characters',
|
||||
passwordRequired: 'Set a VNC password',
|
||||
passwordMaxLength: 'VNC passwords are limited to 8 characters',
|
||||
passwordSaved: 'Password is saved; leaving this empty keeps it unchanged.',
|
||||
clients: '{count} clients',
|
||||
urlPreview: 'VNC Address Preview',
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
title: 'Connection Stats',
|
||||
|
||||
@@ -521,8 +521,7 @@ export default {
|
||||
environmentSubtitle: '系统级运行环境与 USB 设备维护',
|
||||
aboutSubtitle: '在线升级、版本信息与设备硬件概览',
|
||||
extTtydSubtitle: '在浏览器中打开本机 Shell 终端',
|
||||
extRustdeskSubtitle: '通过 RustDesk 实现远程图形访问',
|
||||
extRtspSubtitle: '提供 RTSP 视频流以供其他客户端拉流',
|
||||
thirdPartyAccessSubtitle: '集中配置 RustDesk、VNC 与 RTSP 外部接入',
|
||||
extRemoteAccessSubtitle: '通过内网穿透服务实现远程访问',
|
||||
extFrpcSubtitle: '通过 FRP 客户端实现内网穿透',
|
||||
aboutDesc: '开放轻量的 IP-KVM 解决方案',
|
||||
@@ -966,6 +965,10 @@ export default {
|
||||
start: '启动',
|
||||
stop: '停止',
|
||||
autoStart: '开机自启',
|
||||
thirdPartyAccess: {
|
||||
title: '第三方接入',
|
||||
desc: '集中配置 RustDesk、VNC 与 RTSP',
|
||||
},
|
||||
viewLogs: '查看日志',
|
||||
noLogs: '暂无日志',
|
||||
binaryNotFound: '未找到 {path},请先安装对应程序',
|
||||
@@ -1039,6 +1042,8 @@ export default {
|
||||
relayServer: '中继服务器',
|
||||
relayServerPlaceholder: 'hbbr.example.com:21117',
|
||||
relayKey: '中继密钥',
|
||||
codec: '编码格式',
|
||||
codecHint: 'RustDesk 启动前需选择 H.264 或 H.265;运行时会锁定编码,不允许客户端切换。',
|
||||
deviceInfo: '设备信息',
|
||||
deviceId: '设备 ID',
|
||||
deviceIdHint: '此 ID 用于 RustDesk 客户端连接',
|
||||
@@ -1072,7 +1077,7 @@ export default {
|
||||
pathPlaceholder: 'live',
|
||||
pathHint: '访问路径,例如 rtsp://设备IP:8554/live',
|
||||
codec: '编码格式',
|
||||
codecHint: '启用 RTSP 后将锁定编码为所选项,并禁用 MJPEG。',
|
||||
codecHint: 'RTSP 运行时会锁定为所选编码;若 RustDesk 已运行,只能选择相同编码。',
|
||||
allowOneClient: '仅允许单客户端',
|
||||
username: '用户名',
|
||||
usernamePlaceholder: '留空表示无需认证',
|
||||
@@ -1080,6 +1085,26 @@ export default {
|
||||
passwordPlaceholder: '输入新密码',
|
||||
urlPreview: 'RTSP 地址预览',
|
||||
},
|
||||
vnc: {
|
||||
title: 'VNC 远程',
|
||||
desc: '通过 TigerVNC 客户端访问',
|
||||
bind: '监听地址',
|
||||
port: '端口',
|
||||
encoding: '视频编码',
|
||||
encodingTightJpeg: 'Tight JPEG',
|
||||
encodingH264: 'H.264',
|
||||
encodingHint: 'VNC 运行时会锁定编码;H.265 锁定时 VNC 无法启动,MJPEG 锁定时 RTSP 与 RustDesk 无法启动。',
|
||||
jpegQuality: 'JPEG 质量',
|
||||
allowOneClient: '仅允许单客户端',
|
||||
password: '密码',
|
||||
passwordPlaceholder: '留空表示不修改',
|
||||
passwordRequiredPlaceholder: '最多 8 个字符',
|
||||
passwordRequired: '请设置 VNC 密码',
|
||||
passwordMaxLength: 'VNC 密码最多 8 个字符',
|
||||
passwordSaved: '已保存密码;留空不会修改。',
|
||||
clients: '{count} 个客户端',
|
||||
urlPreview: 'VNC 地址预览',
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
title: '连接统计',
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
rtspConfigApi,
|
||||
rustdeskConfigApi,
|
||||
streamConfigApi,
|
||||
vncConfigApi,
|
||||
videoConfigApi,
|
||||
webConfigApi,
|
||||
} from '@/api'
|
||||
@@ -36,6 +37,9 @@ import type {
|
||||
RustDeskConfigUpdate as ApiRustDeskConfigUpdate,
|
||||
RustDeskStatusResponse as ApiRustDeskStatusResponse,
|
||||
RustDeskPasswordResponse as ApiRustDeskPasswordResponse,
|
||||
VncConfigResponse as ApiVncConfigResponse,
|
||||
VncConfigUpdate as ApiVncConfigUpdate,
|
||||
VncStatusResponse as ApiVncStatusResponse,
|
||||
WebConfig,
|
||||
WebConfigUpdate,
|
||||
} from '@/api'
|
||||
@@ -57,6 +61,8 @@ export const useConfigStore = defineStore('config', () => {
|
||||
const atx = ref<AtxConfig | null>(null)
|
||||
const rtspConfig = ref<ApiRtspConfigResponse | null>(null)
|
||||
const rtspStatus = ref<ApiRtspStatusResponse | null>(null)
|
||||
const vncConfig = ref<ApiVncConfigResponse | null>(null)
|
||||
const vncStatus = ref<ApiVncStatusResponse | null>(null)
|
||||
const rustdeskConfig = ref<ApiRustDeskConfigResponse | null>(null)
|
||||
const rustdeskStatus = ref<ApiRustDeskStatusResponse | null>(null)
|
||||
const rustdeskPassword = ref<ApiRustDeskPasswordResponse | null>(null)
|
||||
@@ -70,6 +76,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
const webLoading = ref(false)
|
||||
const atxLoading = ref(false)
|
||||
const rtspLoading = ref(false)
|
||||
const vncLoading = ref(false)
|
||||
const rustdeskLoading = ref(false)
|
||||
|
||||
const authError = ref<string | null>(null)
|
||||
@@ -81,6 +88,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
const webError = ref<string | null>(null)
|
||||
const atxError = ref<string | null>(null)
|
||||
const rtspError = ref<string | null>(null)
|
||||
const vncError = ref<string | null>(null)
|
||||
const rustdeskError = ref<string | null>(null)
|
||||
|
||||
let authPromise: Promise<AuthConfig> | null = null
|
||||
@@ -93,6 +101,8 @@ export const useConfigStore = defineStore('config', () => {
|
||||
let atxPromise: Promise<AtxConfig> | null = null
|
||||
let rtspPromise: Promise<ApiRtspConfigResponse> | null = null
|
||||
let rtspStatusPromise: Promise<ApiRtspStatusResponse> | null = null
|
||||
let vncPromise: Promise<ApiVncConfigResponse> | null = null
|
||||
let vncStatusPromise: Promise<ApiVncStatusResponse> | null = null
|
||||
let rustdeskPromise: Promise<ApiRustDeskConfigResponse> | null = null
|
||||
let rustdeskStatusPromise: Promise<ApiRustDeskStatusResponse> | null = null
|
||||
let rustdeskPasswordPromise: Promise<ApiRustDeskPasswordResponse> | null = null
|
||||
@@ -318,6 +328,51 @@ export const useConfigStore = defineStore('config', () => {
|
||||
return request
|
||||
}
|
||||
|
||||
async function refreshVncConfig() {
|
||||
if (vncLoading.value && vncPromise) return vncPromise
|
||||
vncLoading.value = true
|
||||
vncError.value = null
|
||||
const request = vncConfigApi.get()
|
||||
.then((response) => {
|
||||
vncConfig.value = response
|
||||
return response
|
||||
})
|
||||
.catch((error) => {
|
||||
vncError.value = normalizeErrorMessage(error)
|
||||
throw error
|
||||
})
|
||||
.finally(() => {
|
||||
vncLoading.value = false
|
||||
vncPromise = null
|
||||
})
|
||||
|
||||
vncPromise = request
|
||||
return request
|
||||
}
|
||||
|
||||
async function refreshVncStatus() {
|
||||
if (vncLoading.value && vncStatusPromise) return vncStatusPromise
|
||||
vncLoading.value = true
|
||||
vncError.value = null
|
||||
const request = vncConfigApi.getStatus()
|
||||
.then((response) => {
|
||||
vncStatus.value = response
|
||||
vncConfig.value = response.config
|
||||
return response
|
||||
})
|
||||
.catch((error) => {
|
||||
vncError.value = normalizeErrorMessage(error)
|
||||
throw error
|
||||
})
|
||||
.finally(() => {
|
||||
vncLoading.value = false
|
||||
vncStatusPromise = null
|
||||
})
|
||||
|
||||
vncStatusPromise = request
|
||||
return request
|
||||
}
|
||||
|
||||
async function refreshRustdeskConfig() {
|
||||
if (rustdeskLoading.value && rustdeskPromise) return rustdeskPromise
|
||||
rustdeskLoading.value = true
|
||||
@@ -430,6 +485,11 @@ export const useConfigStore = defineStore('config', () => {
|
||||
return refreshRtspConfig()
|
||||
}
|
||||
|
||||
function ensureVncConfig() {
|
||||
if (vncConfig.value) return Promise.resolve(vncConfig.value)
|
||||
return refreshVncConfig()
|
||||
}
|
||||
|
||||
function ensureRustdeskConfig() {
|
||||
if (rustdeskConfig.value) return Promise.resolve(rustdeskConfig.value)
|
||||
return refreshRustdeskConfig()
|
||||
@@ -489,6 +549,12 @@ export const useConfigStore = defineStore('config', () => {
|
||||
return response
|
||||
}
|
||||
|
||||
async function updateVnc(update: ApiVncConfigUpdate) {
|
||||
const response = await vncConfigApi.update(update)
|
||||
vncConfig.value = response
|
||||
return response
|
||||
}
|
||||
|
||||
async function updateRustdesk(update: ApiRustDeskConfigUpdate) {
|
||||
const response = await rustdeskConfigApi.update(update)
|
||||
rustdeskConfig.value = response
|
||||
@@ -518,6 +584,8 @@ export const useConfigStore = defineStore('config', () => {
|
||||
atx,
|
||||
rtspConfig,
|
||||
rtspStatus,
|
||||
vncConfig,
|
||||
vncStatus,
|
||||
rustdeskConfig,
|
||||
rustdeskStatus,
|
||||
rustdeskPassword,
|
||||
@@ -530,6 +598,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
webLoading,
|
||||
atxLoading,
|
||||
rtspLoading,
|
||||
vncLoading,
|
||||
rustdeskLoading,
|
||||
authError,
|
||||
videoError,
|
||||
@@ -540,6 +609,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
webError,
|
||||
atxError,
|
||||
rtspError,
|
||||
vncError,
|
||||
rustdeskError,
|
||||
refreshAuth,
|
||||
refreshVideo,
|
||||
@@ -551,6 +621,8 @@ export const useConfigStore = defineStore('config', () => {
|
||||
refreshAtx,
|
||||
refreshRtspConfig,
|
||||
refreshRtspStatus,
|
||||
refreshVncConfig,
|
||||
refreshVncStatus,
|
||||
refreshRustdeskConfig,
|
||||
refreshRustdeskStatus,
|
||||
refreshRustdeskPassword,
|
||||
@@ -563,6 +635,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
ensureWeb,
|
||||
ensureAtx,
|
||||
ensureRtspConfig,
|
||||
ensureVncConfig,
|
||||
ensureRustdeskConfig,
|
||||
updateAuth,
|
||||
updateVideo,
|
||||
@@ -573,6 +646,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
updateWeb,
|
||||
updateAtx,
|
||||
updateRtsp,
|
||||
updateVnc,
|
||||
updateRustdesk,
|
||||
regenerateRustdeskId,
|
||||
regenerateRustdeskPassword,
|
||||
|
||||
@@ -62,14 +62,6 @@ export interface Ch9329DescriptorConfig {
|
||||
serial_number?: string;
|
||||
}
|
||||
|
||||
export interface Ch9329DescriptorState {
|
||||
descriptor: Ch9329DescriptorConfig;
|
||||
manufacturer_enabled: boolean;
|
||||
product_enabled: boolean;
|
||||
serial_enabled: boolean;
|
||||
config_mode_available: boolean;
|
||||
}
|
||||
|
||||
export interface HidConfig {
|
||||
backend: HidBackend;
|
||||
otg_udc?: string;
|
||||
@@ -234,11 +226,31 @@ export interface ExtensionsConfig {
|
||||
|
||||
export interface RustDeskConfig {
|
||||
enabled: boolean;
|
||||
codec: RustDeskCodec;
|
||||
rendezvous_server: string;
|
||||
relay_server?: string;
|
||||
device_id: string;
|
||||
}
|
||||
|
||||
export enum RustDeskCodec {
|
||||
H264 = "h264",
|
||||
H265 = "h265",
|
||||
}
|
||||
|
||||
export enum VncEncoding {
|
||||
TightJpeg = "tight_jpeg",
|
||||
H264 = "h264",
|
||||
}
|
||||
|
||||
export interface VncConfig {
|
||||
enabled: boolean;
|
||||
bind: string;
|
||||
port: number;
|
||||
encoding: VncEncoding;
|
||||
jpeg_quality: number;
|
||||
allow_one_client: boolean;
|
||||
}
|
||||
|
||||
export enum RtspCodec {
|
||||
H264 = "h264",
|
||||
H265 = "h265",
|
||||
@@ -270,6 +282,7 @@ export interface AppConfig {
|
||||
web: WebConfig;
|
||||
extensions: ExtensionsConfig;
|
||||
rustdesk: RustDeskConfig;
|
||||
vnc: VncConfig;
|
||||
rtsp: RtspConfig;
|
||||
redfish: RedfishConfig;
|
||||
}
|
||||
@@ -328,6 +341,14 @@ export interface Ch9329DescriptorConfigUpdate {
|
||||
serial_number?: string;
|
||||
}
|
||||
|
||||
export interface Ch9329DescriptorState {
|
||||
descriptor: Ch9329DescriptorConfig;
|
||||
manufacturer_enabled: boolean;
|
||||
product_enabled: boolean;
|
||||
serial_enabled: boolean;
|
||||
config_mode_available: boolean;
|
||||
}
|
||||
|
||||
export interface EasytierConfigUpdate {
|
||||
enabled?: boolean;
|
||||
network_name?: string;
|
||||
@@ -480,6 +501,7 @@ export interface RtspStatusResponse {
|
||||
|
||||
export interface RustDeskConfigUpdate {
|
||||
enabled?: boolean;
|
||||
codec?: RustDeskCodec;
|
||||
rendezvous_server?: string;
|
||||
relay_server?: string;
|
||||
relay_key?: string;
|
||||
@@ -535,6 +557,32 @@ export interface VideoConfigUpdate {
|
||||
quality?: number;
|
||||
}
|
||||
|
||||
export interface VncConfigResponse {
|
||||
enabled: boolean;
|
||||
bind: string;
|
||||
port: number;
|
||||
encoding: VncEncoding;
|
||||
jpeg_quality: number;
|
||||
allow_one_client: boolean;
|
||||
has_password: boolean;
|
||||
}
|
||||
|
||||
export interface VncConfigUpdate {
|
||||
enabled?: boolean;
|
||||
bind?: string;
|
||||
port?: number;
|
||||
encoding?: VncEncoding;
|
||||
jpeg_quality?: number;
|
||||
allow_one_client?: boolean;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface VncStatusResponse {
|
||||
config: VncConfigResponse;
|
||||
service_status: string;
|
||||
connection_count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Web server settings returned by `GET` / `PATCH /api/config/web`.
|
||||
*
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
systemApi,
|
||||
updateApi,
|
||||
usbApi,
|
||||
vncConfigApi,
|
||||
type EncoderBackendInfo,
|
||||
type AuthConfig,
|
||||
type RustDeskConfigResponse,
|
||||
@@ -27,6 +28,8 @@ import {
|
||||
type RustDeskPasswordResponse,
|
||||
type RtspStatusResponse,
|
||||
type RtspConfigUpdate,
|
||||
type VncConfigUpdate,
|
||||
type VncStatusResponse,
|
||||
type WebConfig,
|
||||
type UpdateOverviewResponse,
|
||||
type UpdateStatusResponse,
|
||||
@@ -105,7 +108,6 @@ import {
|
||||
ExternalLink,
|
||||
Copy,
|
||||
ScreenShare,
|
||||
Radio,
|
||||
Globe,
|
||||
Loader2,
|
||||
AlertTriangle,
|
||||
@@ -136,8 +138,7 @@ const SETTINGS_SECTION_IDS = [
|
||||
'atx',
|
||||
'environment',
|
||||
'ext-ttyd',
|
||||
'ext-rustdesk',
|
||||
'ext-rtsp',
|
||||
'third-party-access',
|
||||
'ext-remote-access',
|
||||
'about',
|
||||
] as const
|
||||
@@ -167,8 +168,7 @@ const navGroups = computed(() => [
|
||||
title: t('settings.extensions'),
|
||||
items: [
|
||||
{ id: 'ext-ttyd', label: t('extensions.ttyd.title'), icon: Terminal },
|
||||
{ id: 'ext-rustdesk', label: t('extensions.rustdesk.title'), icon: ScreenShare },
|
||||
{ id: 'ext-rtsp', label: t('extensions.rtsp.title'), icon: Radio },
|
||||
{ id: 'third-party-access', label: t('extensions.thirdPartyAccess.title'), icon: ScreenShare },
|
||||
{ id: 'ext-remote-access', label: t('extensions.remoteAccess.title'), icon: ExternalLink },
|
||||
]
|
||||
},
|
||||
@@ -200,8 +200,7 @@ const sectionMeta = computed(() => {
|
||||
function sectionSubtitleKey(id: string): string {
|
||||
switch (id) {
|
||||
case 'ext-ttyd': return 'extTtydSubtitle'
|
||||
case 'ext-rustdesk': return 'extRustdeskSubtitle'
|
||||
case 'ext-rtsp': return 'extRtspSubtitle'
|
||||
case 'third-party-access': return 'thirdPartyAccessSubtitle'
|
||||
case 'ext-remote-access': return 'extRemoteAccessSubtitle'
|
||||
default: return `${id}Subtitle`
|
||||
}
|
||||
@@ -222,6 +221,7 @@ function normalizeSettingsSection(value: unknown): SettingsSectionId | null {
|
||||
if (typeof value !== 'string') return null
|
||||
if (value === 'access-control') return 'account'
|
||||
if (value === 'ext-frpc') return 'ext-remote-access'
|
||||
if (value === 'ext-rustdesk' || value === 'ext-vnc' || value === 'ext-rtsp') return 'third-party-access'
|
||||
return isSettingsSectionId(value) ? value : null
|
||||
}
|
||||
|
||||
@@ -272,15 +272,14 @@ async function loadSectionData(section: SettingsSectionId) {
|
||||
case 'ext-remote-access':
|
||||
await loadExtensions()
|
||||
return
|
||||
case 'ext-rustdesk':
|
||||
case 'third-party-access':
|
||||
await Promise.all([
|
||||
loadRustdeskConfig(),
|
||||
loadRustdeskPassword(),
|
||||
loadRtspConfig(),
|
||||
loadVncConfig(),
|
||||
])
|
||||
return
|
||||
case 'ext-rtsp':
|
||||
await loadRtspConfig()
|
||||
return
|
||||
case 'about':
|
||||
if (isAndroid.value) return
|
||||
await Promise.all([
|
||||
@@ -390,6 +389,7 @@ const rustdeskCopied = ref<'id' | 'password' | null>(null)
|
||||
const { copy: clipboardCopy } = useClipboard()
|
||||
const rustdeskLocalConfig = ref({
|
||||
enabled: false,
|
||||
codec: 'h264' as 'h264' | 'h265',
|
||||
rendezvous_server: '',
|
||||
relay_server: '',
|
||||
relay_key: '',
|
||||
@@ -415,6 +415,18 @@ const rtspLocalConfig = ref<RtspConfigUpdate & { password?: string }>({
|
||||
password: '',
|
||||
})
|
||||
|
||||
const vncStatus = ref<VncStatusResponse | null>(null)
|
||||
const vncLoading = ref(false)
|
||||
const vncLocalConfig = ref<VncConfigUpdate & { password?: string }>({
|
||||
enabled: false,
|
||||
bind: '0.0.0.0',
|
||||
port: 5900,
|
||||
encoding: 'tight_jpeg',
|
||||
jpeg_quality: 80,
|
||||
allow_one_client: true,
|
||||
password: '',
|
||||
})
|
||||
|
||||
function formatHostForUrl(hostname: string): string {
|
||||
if (!hostname) return '127.0.0.1'
|
||||
return hostname.includes(':') && !hostname.startsWith('[')
|
||||
@@ -429,6 +441,12 @@ const rtspStreamUrl = computed(() => {
|
||||
return `rtsp://${host}:${port}/${path}`
|
||||
})
|
||||
|
||||
const vncStreamUrl = computed(() => {
|
||||
const host = formatHostForUrl(window.location.hostname || '127.0.0.1')
|
||||
const port = Number(vncLocalConfig.value.port) || 5900
|
||||
return `${host}:${port}`
|
||||
})
|
||||
|
||||
const webServerConfig = ref<WebConfig>({
|
||||
http_port: 8080,
|
||||
https_port: 8443,
|
||||
@@ -1839,6 +1857,7 @@ function applyRustdeskStatus(status: RustDeskStatusResponse) {
|
||||
rustdeskStatus.value = status
|
||||
rustdeskLocalConfig.value = {
|
||||
enabled: config.enabled,
|
||||
codec: config.codec || 'h264',
|
||||
rendezvous_server: config.rendezvous_server,
|
||||
relay_server: config.relay_server || '',
|
||||
relay_key: config.relay_key || '',
|
||||
@@ -1901,6 +1920,17 @@ function validateRustdeskConfig(): boolean {
|
||||
return !rustdeskValidationMessage.value || showValidationError(rustdeskValidationMessage.value)
|
||||
}
|
||||
|
||||
function validateVncConfig(enabled = vncLocalConfig.value.enabled): boolean {
|
||||
const password = (vncLocalConfig.value.password || '').trim()
|
||||
if (enabled && !vncStatus.value?.config.has_password && !password) {
|
||||
return showValidationError(t('extensions.vnc.passwordRequired'))
|
||||
}
|
||||
if (password.length > 8) {
|
||||
return showValidationError(t('extensions.vnc.passwordMaxLength'))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function normalizeRtspPath(path: string): string {
|
||||
return path.trim().replace(/^\/+|\/+$/g, '') || 'live'
|
||||
}
|
||||
@@ -2222,23 +2252,26 @@ function updateStatusBadgeText(): string {
|
||||
|| updatePhaseText(updateStatus.value?.phase)
|
||||
}
|
||||
|
||||
function rustdeskUpdatePayload(enabled = rustdeskLocalConfig.value.enabled) {
|
||||
return {
|
||||
enabled,
|
||||
codec: rustdeskLocalConfig.value.codec,
|
||||
rendezvous_server: normalizeRustdeskServer(
|
||||
rustdeskLocalConfig.value.rendezvous_server,
|
||||
21116,
|
||||
),
|
||||
relay_server: normalizeRustdeskServer(rustdeskLocalConfig.value.relay_server, 21117),
|
||||
relay_key: normalizeRustdeskRelayKey(rustdeskLocalConfig.value.relay_key),
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRustdeskConfig() {
|
||||
if (rustdeskLocalConfig.value.enabled && !validateRustdeskConfig()) return
|
||||
|
||||
loading.value = true
|
||||
saved.value = false
|
||||
try {
|
||||
const rendezvousServer = normalizeRustdeskServer(
|
||||
rustdeskLocalConfig.value.rendezvous_server,
|
||||
21116,
|
||||
)
|
||||
const relayServer = normalizeRustdeskServer(rustdeskLocalConfig.value.relay_server, 21117)
|
||||
await configStore.updateRustdesk({
|
||||
enabled: rustdeskLocalConfig.value.enabled,
|
||||
rendezvous_server: rendezvousServer,
|
||||
relay_server: relayServer,
|
||||
relay_key: normalizeRustdeskRelayKey(rustdeskLocalConfig.value.relay_key),
|
||||
})
|
||||
await configStore.updateRustdesk(rustdeskUpdatePayload())
|
||||
await loadRustdeskConfig()
|
||||
saved.value = true
|
||||
setTimeout(() => (saved.value = false), 2000)
|
||||
@@ -2279,6 +2312,7 @@ async function startRustdesk() {
|
||||
|
||||
rustdeskLoading.value = true
|
||||
try {
|
||||
await configStore.updateRustdesk(rustdeskUpdatePayload(true))
|
||||
const status = await rustdeskConfigApi.start()
|
||||
applyRustdeskStatus(status)
|
||||
} catch {
|
||||
@@ -2373,23 +2407,24 @@ async function loadRtspConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
function rtspUpdatePayload(enabled = !!rtspLocalConfig.value.enabled): RtspConfigUpdate {
|
||||
return {
|
||||
enabled,
|
||||
bind: rtspLocalConfig.value.bind?.trim() || '0.0.0.0',
|
||||
port: Number(rtspLocalConfig.value.port) || 8554,
|
||||
path: normalizeRtspPath(rtspLocalConfig.value.path || 'live'),
|
||||
allow_one_client: !!rtspLocalConfig.value.allow_one_client,
|
||||
codec: rtspLocalConfig.value.codec || 'h264',
|
||||
username: (rtspLocalConfig.value.username || '').trim(),
|
||||
password: (rtspLocalConfig.value.password || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRtspConfig() {
|
||||
loading.value = true
|
||||
saved.value = false
|
||||
try {
|
||||
const update: RtspConfigUpdate = {
|
||||
enabled: !!rtspLocalConfig.value.enabled,
|
||||
bind: rtspLocalConfig.value.bind?.trim() || '0.0.0.0',
|
||||
port: Number(rtspLocalConfig.value.port) || 8554,
|
||||
path: normalizeRtspPath(rtspLocalConfig.value.path || 'live'),
|
||||
allow_one_client: !!rtspLocalConfig.value.allow_one_client,
|
||||
codec: rtspLocalConfig.value.codec || 'h264',
|
||||
username: (rtspLocalConfig.value.username || '').trim(),
|
||||
}
|
||||
|
||||
update.password = (rtspLocalConfig.value.password || '').trim()
|
||||
|
||||
await configStore.updateRtsp(update)
|
||||
await configStore.updateRtsp(rtspUpdatePayload())
|
||||
await loadRtspConfig()
|
||||
saved.value = true
|
||||
setTimeout(() => (saved.value = false), 2000)
|
||||
@@ -2402,6 +2437,7 @@ async function saveRtspConfig() {
|
||||
async function startRtsp() {
|
||||
rtspLoading.value = true
|
||||
try {
|
||||
await configStore.updateRtsp(rtspUpdatePayload(true))
|
||||
const status = await rtspConfigApi.start()
|
||||
applyRtspStatus(status)
|
||||
} catch {
|
||||
@@ -2421,6 +2457,108 @@ async function stopRtsp() {
|
||||
}
|
||||
}
|
||||
|
||||
function applyVncStatus(status: VncStatusResponse) {
|
||||
vncStatus.value = status
|
||||
vncLocalConfig.value = {
|
||||
enabled: status.config.enabled,
|
||||
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: '',
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVncConfig() {
|
||||
vncLoading.value = true
|
||||
try {
|
||||
const status = await configStore.refreshVncStatus()
|
||||
applyVncStatus(status)
|
||||
} catch {
|
||||
} finally {
|
||||
vncLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function vncUpdatePayload(enabled = !!vncLocalConfig.value.enabled): VncConfigUpdate {
|
||||
const update: VncConfigUpdate = {
|
||||
enabled,
|
||||
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()
|
||||
if (password) update.password = password
|
||||
return update
|
||||
}
|
||||
|
||||
async function saveVncConfig() {
|
||||
if (!validateVncConfig()) return
|
||||
|
||||
loading.value = true
|
||||
saved.value = false
|
||||
try {
|
||||
await configStore.updateVnc(vncUpdatePayload())
|
||||
await loadVncConfig()
|
||||
saved.value = true
|
||||
setTimeout(() => (saved.value = false), 2000)
|
||||
} catch {
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function startVnc() {
|
||||
if (!validateVncConfig(true)) return
|
||||
|
||||
vncLoading.value = true
|
||||
try {
|
||||
await configStore.updateVnc(vncUpdatePayload(true))
|
||||
const status = await vncConfigApi.start()
|
||||
applyVncStatus(status)
|
||||
} catch {
|
||||
} finally {
|
||||
vncLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function stopVnc() {
|
||||
vncLoading.value = true
|
||||
try {
|
||||
const status = await vncConfigApi.stop()
|
||||
applyVncStatus(status)
|
||||
} catch {
|
||||
} finally {
|
||||
vncLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getVncServiceStatusText(status: string | undefined): string {
|
||||
if (!status) return t('extensions.stopped')
|
||||
switch (status) {
|
||||
case 'running': return t('extensions.running')
|
||||
case 'starting': return t('extensions.starting')
|
||||
case 'stopped': return t('extensions.stopped')
|
||||
default:
|
||||
if (status.startsWith('error:')) return t('extensions.failed')
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
function getVncStatusClass(status: string | undefined): string {
|
||||
switch (status) {
|
||||
case 'running': return 'bg-green-500'
|
||||
case 'starting': return 'bg-yellow-500'
|
||||
case 'stopped': return 'bg-gray-400'
|
||||
default:
|
||||
if (status?.startsWith('error:')) return 'bg-red-500'
|
||||
return 'bg-gray-400'
|
||||
}
|
||||
}
|
||||
|
||||
function getRtspServiceStatusText(status: string | undefined): string {
|
||||
if (!status) return t('extensions.stopped')
|
||||
switch (status) {
|
||||
@@ -4506,7 +4644,7 @@ watch(isWindows, () => {
|
||||
</div>
|
||||
|
||||
<!-- RTSP Section -->
|
||||
<div v-show="activeSection === 'ext-rtsp'" class="space-y-6">
|
||||
<div v-show="activeSection === 'third-party-access'" class="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -4631,8 +4769,134 @@ watch(isWindows, () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VNC Section -->
|
||||
<div v-show="activeSection === 'third-party-access'" class="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-1.5">
|
||||
<CardTitle>{{ t('extensions.vnc.title') }}</CardTitle>
|
||||
<CardDescription>{{ t('extensions.vnc.desc') }}</CardDescription>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge :variant="vncStatus?.service_status === 'running' ? 'default' : 'secondary'">
|
||||
{{ getVncServiceStatusText(vncStatus?.service_status) }}
|
||||
</Badge>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadVncConfig" :disabled="vncLoading">
|
||||
<RefreshCw :class="['h-4 w-4', vncLoading ? 'animate-spin' : '']" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div :class="['w-2 h-2 rounded-full', getVncStatusClass(vncStatus?.service_status)]" />
|
||||
<span class="text-sm">{{ getVncServiceStatusText(vncStatus?.service_status) }}</span>
|
||||
<template v-if="vncStatus?.connection_count">
|
||||
<span class="text-muted-foreground">|</span>
|
||||
<span class="text-sm text-muted-foreground">{{ t('extensions.vnc.clients', { count: vncStatus.connection_count }) }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
v-if="vncStatus?.service_status !== 'running' && vncStatus?.service_status !== 'starting'"
|
||||
size="sm"
|
||||
@click="startVnc"
|
||||
:disabled="vncLoading || vncStatus?.service_status === 'starting'"
|
||||
>
|
||||
<Play class="h-4 w-4 mr-1" />
|
||||
{{ t('extensions.start') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@click="stopVnc"
|
||||
:disabled="vncLoading"
|
||||
>
|
||||
<Square class="h-4 w-4 mr-1" />
|
||||
{{ t('extensions.stop') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label>{{ t('extensions.autoStart') }}</Label>
|
||||
<Switch v-model="vncLocalConfig.enabled" :disabled="vncStatus?.service_status === 'running'" />
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.vnc.bind') }}</Label>
|
||||
<Input v-model="vncLocalConfig.bind" class="sm:col-span-3" placeholder="0.0.0.0" :disabled="vncStatus?.service_status === 'running'" />
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.vnc.port') }}</Label>
|
||||
<Input v-model.number="vncLocalConfig.port" class="sm:col-span-3" type="number" min="1" max="65535" :disabled="vncStatus?.service_status === 'running'" />
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.vnc.encoding') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
<select v-model="vncLocalConfig.encoding" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="vncStatus?.service_status === 'running'">
|
||||
<option value="tight_jpeg">{{ t('extensions.vnc.encodingTightJpeg') }}</option>
|
||||
<option value="h264">{{ t('extensions.vnc.encodingH264') }}</option>
|
||||
</select>
|
||||
<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'" />
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.vnc.password') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
<div class="relative">
|
||||
<Input
|
||||
v-model="vncLocalConfig.password"
|
||||
:type="showPasswords ? 'text' : 'password'"
|
||||
maxlength="8"
|
||||
autocomplete="off"
|
||||
:placeholder="vncStatus?.config.has_password ? t('extensions.vnc.passwordPlaceholder') : t('extensions.vnc.passwordRequiredPlaceholder')"
|
||||
:disabled="vncStatus?.service_status === 'running'"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground"
|
||||
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
|
||||
@click="showPasswords = !showPasswords"
|
||||
>
|
||||
<Eye v-if="!showPasswords" class="h-4 w-4" />
|
||||
<EyeOff v-else class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="vncStatus?.config.has_password" class="text-xs text-muted-foreground">{{ t('extensions.vnc.passwordSaved') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="rounded-md border p-3 bg-muted/20 space-y-1">
|
||||
<p class="text-sm font-medium">{{ t('extensions.vnc.urlPreview') }}</p>
|
||||
<code class="font-mono text-sm break-all">{{ vncStreamUrl }}</code>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div class="flex justify-end">
|
||||
<Button :disabled="loading || vncLoading || vncStatus?.service_status === 'running'" @click="saveVncConfig">
|
||||
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RustDesk Section -->
|
||||
<div v-show="activeSection === 'ext-rustdesk'" class="space-y-6">
|
||||
<div v-show="activeSection === 'third-party-access'" class="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -4692,6 +4956,16 @@ watch(isWindows, () => {
|
||||
<Label>{{ t('extensions.autoStart') }}</Label>
|
||||
<Switch v-model="rustdeskLocalConfig.enabled" :disabled="rustdeskStatus?.service_status === 'running'" />
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.rustdesk.codec') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
<select v-model="rustdeskLocalConfig.codec" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="rustdeskStatus?.service_status === 'running'">
|
||||
<option value="h264">H.264</option>
|
||||
<option value="h265">H.265</option>
|
||||
</select>
|
||||
<p class="text-xs text-muted-foreground">{{ t('extensions.rustdesk.codecHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.rustdesk.rendezvousServer') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
|
||||
Reference in New Issue
Block a user