feat(rustdesk): 优化视频编码协商和添加公共服务器支持

- 调整视频编码优先级为 H264 > H265 > VP8 > VP9,优先使用硬件编码
- 对接 RustDesk 客户端质量预设 (Low/Balanced/Best) 到 BitratePreset
- 添加 secrets.toml 编译时读取机制,支持配置公共服务器
- 默认公共服务器: rustdesk.mofeng.run:21116
- 前端 ID 服务器输入框添加问号提示,显示公共服务器信息
- 用户留空时自动使用公共服务器
This commit is contained in:
mofeng-git
2026-01-02 17:22:34 +08:00
parent be4de59f3b
commit 28ecf951df
29 changed files with 776 additions and 316 deletions

View File

@@ -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 配置更新 */

View File

@@ -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

View File

@@ -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>

View File

@@ -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',

View File

@@ -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: '绝对定位模式直接映射鼠标坐标,适用于大多数场景',

View File

@@ -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") */

View File

@@ -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">