mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 00:51:53 +08:00
feat(rustdesk): 优化视频编码协商和添加公共服务器支持
- 调整视频编码优先级为 H264 > H265 > VP8 > VP9,优先使用硬件编码 - 对接 RustDesk 客户端质量预设 (Low/Balanced/Best) 到 BitratePreset - 添加 secrets.toml 编译时读取机制,支持配置公共服务器 - 默认公共服务器: rustdesk.mofeng.run:21116 - 前端 ID 服务器输入框添加问号提示,显示公共服务器信息 - 用户留空时自动使用公共服务器
This commit is contained in:
@@ -256,6 +256,12 @@ export const extensionsApi = {
|
||||
|
||||
// ===== RustDesk 配置 API =====
|
||||
|
||||
/** 公共服务器信息 */
|
||||
export interface PublicServerInfo {
|
||||
server: string
|
||||
public_key: string
|
||||
}
|
||||
|
||||
/** RustDesk 配置响应 */
|
||||
export interface RustDeskConfigResponse {
|
||||
enabled: boolean
|
||||
@@ -264,6 +270,7 @@ export interface RustDeskConfigResponse {
|
||||
device_id: string
|
||||
has_password: boolean
|
||||
has_keypair: boolean
|
||||
using_public_server: boolean
|
||||
}
|
||||
|
||||
/** RustDesk 状态响应 */
|
||||
@@ -271,6 +278,7 @@ export interface RustDeskStatusResponse {
|
||||
config: RustDeskConfigResponse
|
||||
service_status: string
|
||||
rendezvous_status: string | null
|
||||
public_server: PublicServerInfo | null
|
||||
}
|
||||
|
||||
/** RustDesk 配置更新 */
|
||||
|
||||
@@ -230,10 +230,10 @@ export const streamApi = {
|
||||
getCodecs: () =>
|
||||
request<AvailableCodecsResponse>('/stream/codecs'),
|
||||
|
||||
setBitrate: (bitrate_kbps: number) =>
|
||||
setBitratePreset: (bitrate_preset: import('@/types/generated').BitratePreset) =>
|
||||
request<{ success: boolean; message?: string }>('/stream/bitrate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ bitrate_kbps }),
|
||||
body: JSON.stringify({ bitrate_preset }),
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -612,6 +612,7 @@ export type {
|
||||
HidBackend,
|
||||
StreamMode,
|
||||
EncoderType,
|
||||
BitratePreset,
|
||||
} from '@/types/generated'
|
||||
|
||||
// Audio API
|
||||
|
||||
@@ -5,7 +5,6 @@ import { toast } from 'vue-sonner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -18,11 +17,10 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Monitor, RefreshCw, Loader2, Settings } from 'lucide-vue-next'
|
||||
import { Monitor, RefreshCw, Loader2, Settings, Zap, Scale, Image } from 'lucide-vue-next'
|
||||
import HelpTooltip from '@/components/HelpTooltip.vue'
|
||||
import { configApi, streamApi, type VideoCodecInfo, type EncoderBackendInfo } from '@/api'
|
||||
import { configApi, streamApi, type VideoCodecInfo, type EncoderBackendInfo, type BitratePreset } from '@/api'
|
||||
import { useSystemStore } from '@/stores/system'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
export type VideoMode = 'mjpeg' | 'h264' | 'h265' | 'vp8' | 'vp9'
|
||||
@@ -179,7 +177,7 @@ const selectedDevice = ref<string>('')
|
||||
const selectedFormat = ref<string>('')
|
||||
const selectedResolution = ref<string>('')
|
||||
const selectedFps = ref<number>(30)
|
||||
const selectedBitrate = ref<number[]>([1000])
|
||||
const selectedBitratePreset = ref<'Speed' | 'Balanced' | 'Quality'>('Balanced')
|
||||
|
||||
// UI state
|
||||
const applying = ref(false)
|
||||
@@ -379,30 +377,27 @@ function handleFpsChange(fps: unknown) {
|
||||
selectedFps.value = typeof fps === 'string' ? Number(fps) : fps
|
||||
}
|
||||
|
||||
// Apply bitrate change (real-time)
|
||||
async function applyBitrate(bitrate: number) {
|
||||
// Apply bitrate preset change
|
||||
async function applyBitratePreset(preset: 'Speed' | 'Balanced' | 'Quality') {
|
||||
if (applyingBitrate.value) return
|
||||
applyingBitrate.value = true
|
||||
try {
|
||||
await streamApi.setBitrate(bitrate)
|
||||
const bitratePreset: BitratePreset = { type: preset }
|
||||
await streamApi.setBitratePreset(bitratePreset)
|
||||
} catch (e) {
|
||||
console.info('[VideoConfig] Failed to apply bitrate:', e)
|
||||
console.info('[VideoConfig] Failed to apply bitrate preset:', e)
|
||||
} finally {
|
||||
applyingBitrate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Debounced bitrate application
|
||||
const debouncedApplyBitrate = useDebounceFn((bitrate: number) => {
|
||||
applyBitrate(bitrate)
|
||||
}, 300)
|
||||
|
||||
// Watch bitrate slider changes (only when in WebRTC mode)
|
||||
watch(selectedBitrate, (newValue) => {
|
||||
if (props.videoMode !== 'mjpeg' && newValue[0] !== undefined) {
|
||||
debouncedApplyBitrate(newValue[0])
|
||||
// Handle bitrate preset selection
|
||||
function handleBitratePresetChange(preset: 'Speed' | 'Balanced' | 'Quality') {
|
||||
selectedBitratePreset.value = preset
|
||||
if (props.videoMode !== 'mjpeg') {
|
||||
applyBitratePreset(preset)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Apply video configuration
|
||||
async function applyVideoConfig() {
|
||||
@@ -529,21 +524,52 @@ watch(() => props.open, (isOpen) => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Bitrate Slider - Only shown for WebRTC modes -->
|
||||
<!-- Bitrate Preset - Only shown for WebRTC modes -->
|
||||
<div v-if="props.videoMode !== 'mjpeg'" class="space-y-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<Label class="text-xs">{{ t('actionbar.bitrate') }}</Label>
|
||||
<HelpTooltip :content="t('help.videoBitrate')" icon-size="sm" />
|
||||
<Label class="text-xs">{{ t('actionbar.bitratePreset') }}</Label>
|
||||
<HelpTooltip :content="t('help.videoBitratePreset')" icon-size="sm" />
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Slider
|
||||
v-model="selectedBitrate"
|
||||
:min="1000"
|
||||
:max="15000"
|
||||
:step="500"
|
||||
class="flex-1"
|
||||
/>
|
||||
<span class="text-xs text-muted-foreground w-20 text-right">{{ selectedBitrate[0] }} kbps</span>
|
||||
<div class="grid grid-cols-3 gap-1.5">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:class="[
|
||||
'h-auto py-1.5 px-2 flex flex-col items-center gap-0.5',
|
||||
selectedBitratePreset === 'Speed' && 'border-primary bg-primary/10'
|
||||
]"
|
||||
:disabled="applyingBitrate"
|
||||
@click="handleBitratePresetChange('Speed')"
|
||||
>
|
||||
<Zap class="h-3.5 w-3.5" />
|
||||
<span class="text-[10px] font-medium">{{ t('actionbar.bitrateSpeed') }}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:class="[
|
||||
'h-auto py-1.5 px-2 flex flex-col items-center gap-0.5',
|
||||
selectedBitratePreset === 'Balanced' && 'border-primary bg-primary/10'
|
||||
]"
|
||||
:disabled="applyingBitrate"
|
||||
@click="handleBitratePresetChange('Balanced')"
|
||||
>
|
||||
<Scale class="h-3.5 w-3.5" />
|
||||
<span class="text-[10px] font-medium">{{ t('actionbar.bitrateBalanced') }}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:class="[
|
||||
'h-auto py-1.5 px-2 flex flex-col items-center gap-0.5',
|
||||
selectedBitratePreset === 'Quality' && 'border-primary bg-primary/10'
|
||||
]"
|
||||
:disabled="applyingBitrate"
|
||||
@click="handleBitratePresetChange('Quality')"
|
||||
>
|
||||
<Image class="h-3.5 w-3.5" />
|
||||
<span class="text-[10px] font-medium">{{ t('actionbar.bitrateQuality') }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -109,7 +109,13 @@ export default {
|
||||
selectFormat: 'Select format...',
|
||||
selectResolution: 'Select resolution...',
|
||||
selectFps: 'Select FPS...',
|
||||
bitrate: 'Bitrate',
|
||||
bitratePreset: 'Bitrate',
|
||||
bitrateSpeed: 'Speed',
|
||||
bitrateSpeedDesc: '1 Mbps - Lowest latency',
|
||||
bitrateBalanced: 'Balanced',
|
||||
bitrateBalancedDesc: '4 Mbps - Recommended',
|
||||
bitrateQuality: 'Quality',
|
||||
bitrateQualityDesc: '8 Mbps - Best visual',
|
||||
browserUnsupported: 'Browser unsupported',
|
||||
encoder: 'Encoder',
|
||||
changeEncoderBackend: 'Change encoder backend...',
|
||||
@@ -649,10 +655,14 @@ export default {
|
||||
serverSettings: 'Server Settings',
|
||||
rendezvousServer: 'ID Server',
|
||||
rendezvousServerPlaceholder: 'hbbs.example.com:21116',
|
||||
rendezvousServerHint: 'RustDesk ID server address (required)',
|
||||
rendezvousServerHint: 'Leave empty to use public server',
|
||||
relayServer: 'Relay Server',
|
||||
relayServerPlaceholder: 'hbbr.example.com:21117',
|
||||
relayServerHint: 'Relay server address, auto-derived from ID server if empty',
|
||||
publicServerInfo: 'Public Server Info',
|
||||
publicServerAddress: 'Server Address',
|
||||
publicServerKey: 'Connection Key',
|
||||
usingPublicServer: 'Using public server',
|
||||
deviceInfo: 'Device Info',
|
||||
deviceId: 'Device ID',
|
||||
deviceIdHint: 'Use this ID in RustDesk client to connect',
|
||||
@@ -721,7 +731,7 @@ export default {
|
||||
// Video related
|
||||
mjpegMode: 'MJPEG mode has best compatibility, works with all browsers, but higher latency',
|
||||
webrtcMode: 'WebRTC mode has lower latency, but requires browser codec support',
|
||||
videoBitrate: 'Higher bitrate means better quality but requires more bandwidth. Adjust based on network',
|
||||
videoBitratePreset: 'Speed: lowest latency, best for slow networks. Balanced: good quality and latency. Quality: best visual, needs good bandwidth',
|
||||
encoderBackend: 'Hardware encoder has better performance and lower power. Software encoder has better compatibility',
|
||||
// HID related
|
||||
absoluteMode: 'Absolute mode maps mouse coordinates directly, suitable for most scenarios',
|
||||
|
||||
@@ -109,7 +109,13 @@ export default {
|
||||
selectFormat: '选择格式...',
|
||||
selectResolution: '选择分辨率...',
|
||||
selectFps: '选择帧率...',
|
||||
bitrate: '码率',
|
||||
bitratePreset: '码率',
|
||||
bitrateSpeed: '速度优先',
|
||||
bitrateSpeedDesc: '1 Mbps - 最低延迟',
|
||||
bitrateBalanced: '均衡',
|
||||
bitrateBalancedDesc: '4 Mbps - 推荐',
|
||||
bitrateQuality: '质量优先',
|
||||
bitrateQualityDesc: '8 Mbps - 最佳画质',
|
||||
browserUnsupported: '浏览器不支持',
|
||||
encoder: '编码器',
|
||||
changeEncoderBackend: '更改编码器后端...',
|
||||
@@ -649,10 +655,14 @@ export default {
|
||||
serverSettings: '服务器设置',
|
||||
rendezvousServer: 'ID 服务器',
|
||||
rendezvousServerPlaceholder: 'hbbs.example.com:21116',
|
||||
rendezvousServerHint: 'RustDesk ID 服务器地址(必填)',
|
||||
rendezvousServerHint: '留空则使用公共服务器',
|
||||
relayServer: '中继服务器',
|
||||
relayServerPlaceholder: 'hbbr.example.com:21117',
|
||||
relayServerHint: '中继服务器地址,留空则自动从 ID 服务器推导',
|
||||
publicServerInfo: '公共服务器信息',
|
||||
publicServerAddress: '服务器地址',
|
||||
publicServerKey: '连接密钥',
|
||||
usingPublicServer: '正在使用公共服务器',
|
||||
deviceInfo: '设备信息',
|
||||
deviceId: '设备 ID',
|
||||
deviceIdHint: '此 ID 用于 RustDesk 客户端连接',
|
||||
@@ -721,7 +731,7 @@ export default {
|
||||
// 视频相关
|
||||
mjpegMode: 'MJPEG 模式兼容性最好,适用于所有浏览器,但延迟较高',
|
||||
webrtcMode: 'WebRTC 模式延迟更低,但需要浏览器支持相应编解码器',
|
||||
videoBitrate: '比特率越高画质越好,但需要更大的网络带宽。建议根据网络状况调整',
|
||||
videoBitratePreset: '速度优先:最低延迟,适合网络较差的场景;均衡:画质和延迟平衡;质量优先:最佳画质,需要较好的网络带宽',
|
||||
encoderBackend: '硬件编码器性能更好功耗更低,软件编码器兼容性更好',
|
||||
// HID 相关
|
||||
absoluteMode: '绝对定位模式直接映射鼠标坐标,适用于大多数场景',
|
||||
|
||||
@@ -183,16 +183,39 @@ export enum EncoderType {
|
||||
V4l2m2m = "v4l2m2m",
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitrate preset for video encoding
|
||||
*
|
||||
* Simplifies bitrate configuration by providing three intuitive presets
|
||||
* plus a custom option for advanced users.
|
||||
*/
|
||||
export type BitratePreset =
|
||||
/**
|
||||
* Speed priority: 1 Mbps, lowest latency, smaller GOP
|
||||
* Best for: slow networks, remote management, low-bandwidth scenarios
|
||||
*/
|
||||
| { type: "Speed", value?: undefined }
|
||||
/**
|
||||
* Balanced: 4 Mbps, good quality/latency tradeoff
|
||||
* Best for: typical usage, recommended default
|
||||
*/
|
||||
| { type: "Balanced", value?: undefined }
|
||||
/**
|
||||
* Quality priority: 8 Mbps, best visual quality
|
||||
* Best for: local network, high-bandwidth scenarios, detailed work
|
||||
*/
|
||||
| { type: "Quality", value?: undefined }
|
||||
/** Custom bitrate in kbps (for advanced users) */
|
||||
| { type: "Custom", value: number };
|
||||
|
||||
/** Streaming configuration */
|
||||
export interface StreamConfig {
|
||||
/** Stream mode */
|
||||
mode: StreamMode;
|
||||
/** Encoder type for H264/H265 */
|
||||
encoder: EncoderType;
|
||||
/** Target bitrate in kbps (for H264/H265) */
|
||||
bitrate_kbps: number;
|
||||
/** GOP size */
|
||||
gop_size: number;
|
||||
/** Bitrate preset (Speed/Balanced/Quality) */
|
||||
bitrate_preset: BitratePreset;
|
||||
/** Custom STUN server (e.g., "stun:stun.l.google.com:19302") */
|
||||
stun_server?: string;
|
||||
/** Custom TURN server (e.g., "turn:turn.example.com:3478") */
|
||||
@@ -264,6 +287,25 @@ export interface ExtensionsConfig {
|
||||
easytier: EasytierConfig;
|
||||
}
|
||||
|
||||
/** RustDesk configuration */
|
||||
export interface RustDeskConfig {
|
||||
/** Enable RustDesk protocol */
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Rendezvous server address (hbbs), e.g., "rs.example.com" or "192.168.1.100"
|
||||
* Port defaults to 21116 if not specified
|
||||
* If empty, uses the public server from secrets.toml
|
||||
*/
|
||||
rendezvous_server: string;
|
||||
/**
|
||||
* Relay server address (hbbr), if different from rendezvous server
|
||||
* Usually the same host as rendezvous server but different port (21117)
|
||||
*/
|
||||
relay_server?: string;
|
||||
/** Device ID (9-digit number), auto-generated if empty */
|
||||
device_id: string;
|
||||
}
|
||||
|
||||
/** Main application configuration */
|
||||
export interface AppConfig {
|
||||
/** Whether initial setup has been completed */
|
||||
@@ -286,6 +328,8 @@ export interface AppConfig {
|
||||
web: WebConfig;
|
||||
/** Extensions settings (ttyd, gostc, easytier) */
|
||||
extensions: ExtensionsConfig;
|
||||
/** RustDesk remote access settings */
|
||||
rustdesk: RustDeskConfig;
|
||||
}
|
||||
|
||||
/** Update for a single ATX key configuration */
|
||||
@@ -441,12 +485,26 @@ export interface MsdConfigUpdate {
|
||||
virtual_drive_size_mb?: number;
|
||||
}
|
||||
|
||||
/** Public server information for display to users */
|
||||
export interface PublicServerInfo {
|
||||
/** Public server address */
|
||||
server: string;
|
||||
/** Public key for client connection */
|
||||
public_key: string;
|
||||
}
|
||||
|
||||
export interface RustDeskConfigUpdate {
|
||||
enabled?: boolean;
|
||||
rendezvous_server?: string;
|
||||
relay_server?: string;
|
||||
device_password?: string;
|
||||
}
|
||||
|
||||
/** Stream 配置响应(包含 has_turn_password 字段) */
|
||||
export interface StreamConfigResponse {
|
||||
mode: StreamMode;
|
||||
encoder: EncoderType;
|
||||
bitrate_kbps: number;
|
||||
gop_size: number;
|
||||
bitrate_preset: BitratePreset;
|
||||
stun_server?: string;
|
||||
turn_server?: string;
|
||||
turn_username?: string;
|
||||
@@ -457,8 +515,7 @@ export interface StreamConfigResponse {
|
||||
export interface StreamConfigUpdate {
|
||||
mode?: StreamMode;
|
||||
encoder?: EncoderType;
|
||||
bitrate_kbps?: number;
|
||||
gop_size?: number;
|
||||
bitrate_preset?: BitratePreset;
|
||||
/** STUN server URL (e.g., "stun:stun.l.google.com:19302") */
|
||||
stun_server?: string;
|
||||
/** TURN server URL (e.g., "turn:turn.example.com:3478") */
|
||||
|
||||
@@ -44,6 +44,12 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import {
|
||||
Monitor,
|
||||
Keyboard,
|
||||
@@ -73,6 +79,7 @@ import {
|
||||
ExternalLink,
|
||||
Copy,
|
||||
ScreenShare,
|
||||
CircleHelp,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
@@ -1825,7 +1832,28 @@ onMounted(async () => {
|
||||
v-model="rustdeskLocalConfig.rendezvous_server"
|
||||
:placeholder="t('extensions.rustdesk.rendezvousServerPlaceholder')"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">{{ t('extensions.rustdesk.rendezvousServerHint') }}</p>
|
||||
<div class="flex items-center gap-1">
|
||||
<p class="text-xs text-muted-foreground">{{ t('extensions.rustdesk.rendezvousServerHint') }}</p>
|
||||
<TooltipProvider v-if="rustdeskStatus?.public_server">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<CircleHelp class="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" class="max-w-xs">
|
||||
<div class="space-y-1.5 text-xs">
|
||||
<p class="font-medium">{{ t('extensions.rustdesk.publicServerInfo') }}</p>
|
||||
<div class="space-y-1">
|
||||
<p><span class="text-muted-foreground">{{ t('extensions.rustdesk.publicServerAddress') }}:</span> {{ rustdeskStatus.public_server.server }}</p>
|
||||
<p><span class="text-muted-foreground">{{ t('extensions.rustdesk.publicServerKey') }}:</span> <code class="text-[10px] break-all">{{ rustdeskStatus.public_server.public_key }}</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<p v-if="rustdeskStatus?.config?.using_public_server" class="text-xs text-blue-500">
|
||||
{{ t('extensions.rustdesk.usingPublicServer') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
|
||||
Reference in New Issue
Block a user