fix: 优化视频切换流畅性;修复 OTG HID 功能无法一次保存成功和页面未即刻生效问题

This commit is contained in:
mofeng-git
2026-04-11 21:20:54 +08:00
parent eecbc0fc13
commit 099f0b1ca2
8 changed files with 219 additions and 73 deletions

View File

@@ -334,15 +334,14 @@ async function addRemoteIceCandidate(candidate: IceCandidate) {
async function flushPendingRemoteIce() {
if (!peerConnection || !sessionId || !peerConnection.remoteDescription) return
const remaining: WebRTCIceCandidateEvent[] = []
for (const event of pendingRemoteCandidates) {
const queued = pendingRemoteCandidates
pendingRemoteCandidates = []
for (const event of queued) {
if (event.session_id === sessionId) {
await addRemoteIceCandidate(event.candidate)
} else {
// Drop candidates for old sessions
}
}
pendingRemoteCandidates = remaining
if (pendingRemoteIceComplete.has(sessionId)) {
pendingRemoteIceComplete.delete(sessionId)
@@ -546,10 +545,8 @@ async function connect(): Promise<boolean> {
}
}
// 等待连接真正建立(最多等待 15 秒)
// 直接检查 peerConnection.connectionState 而不是 reactive state
// 因为 TypeScript 不知道 state 会被 onconnectionstatechange 回调异步修改
const connectionTimeout = 15000
// Wait for connection to establish (5s for LAN, sufficient for most scenarios)
const connectionTimeout = 5000
const pollInterval = 100
let waited = 0
connectStage.value = 'waiting_connection'
@@ -568,7 +565,6 @@ async function connect(): Promise<boolean> {
waited += pollInterval
}
// 超时
throw new Error('Connection timeout waiting for ICE negotiation')
} catch (err) {
state.value = 'failed'

View File

@@ -1312,18 +1312,17 @@ async function switchToWebRTC(codec: VideoMode = 'h264') {
}
}
// Step 3: Connect WebRTC with retry
let retries = 3
// Step 3: Connect WebRTC with retry (backoff between retries)
const MAX_ATTEMPTS = 3
const RETRY_DELAYS = [200, 800]
let success = false
while (retries > 0 && !success) {
success = await connectWebRTCSerial('switchToWebRTC')
if (!success) {
retries--
if (retries > 0) {
console.log(`[WebRTC] Connection failed, retrying (${retries} attempts left)`)
await new Promise(resolve => setTimeout(resolve, 500))
}
for (let attempt = 0; attempt < MAX_ATTEMPTS && !success; attempt++) {
if (attempt > 0) {
const delay = RETRY_DELAYS[attempt - 1] ?? RETRY_DELAYS[RETRY_DELAYS.length - 1]
console.log(`[WebRTC] Connection failed, retrying in ${delay}ms (${MAX_ATTEMPTS - attempt} attempts left)`)
await new Promise(resolve => setTimeout(resolve, delay))
}
success = await connectWebRTCSerial('switchToWebRTC')
}
if (success) {
toast.success(t('console.webrtcConnected'), {
@@ -1526,10 +1525,22 @@ watch(() => webrtc.state.value, (newState, oldState) => {
}, 1000)
}
// Handle direct 'failed' state (ICE or DTLS failure)
// Allow one automatic retry before marking as failed, consistent with
// the disconnected->reconnect path that allows 2 failures.
if (newState === 'failed' && videoMode.value !== 'mjpeg') {
webrtcReconnectFailures += 1
if (webrtcReconnectFailures >= 1) {
if (webrtcReconnectFailures >= 2) {
markWebRTCFailure(t('console.webrtcFailed'))
} else {
webrtcReconnectTimeout = setTimeout(async () => {
if (videoMode.value !== 'mjpeg' && webrtc.state.value !== 'connected') {
const success = await connectWebRTCSerial('auto reconnect after failed')
if (!success) {
markWebRTCFailure(t('console.webrtcFailed'))
}
}
}, 1000)
}
}
})
@@ -2155,6 +2166,10 @@ async function activateConsoleView() {
isConsoleActive.value = true
registerInteractionListeners()
// REST snapshot: returning from Settings (or other routes) may have missed WS device_info
void systemStore.fetchAllStates()
void configStore.refreshHid().then(() => syncMouseModeFromConfig()).catch(() => {})
// Ensure HID WebSocket is connected when console becomes active
if (!hidWs.connected.value) {
hidWs.connect().catch(() => {})

View File

@@ -974,33 +974,29 @@ async function saveConfig() {
saved.value = false
try {
// Save only config related to the active section
const savePromises: Promise<unknown>[] = []
// Save only config related to the active section.
// Sequential awaits: backend ConfigStore uses read-modify-write; parallel PATCH
// requests could overwrite each other's section (last writer wins on full JSON).
// Video config (including encoder and WebRTC/STUN/TURN settings)
if (activeSection.value === 'video') {
savePromises.push(
configStore.updateVideo({
device: config.value.video_device || undefined,
format: config.value.video_format || undefined,
width: config.value.video_width,
height: config.value.video_height,
fps: toConfigFps(config.value.video_fps),
})
)
// Save Stream/Encoder and STUN/TURN config together
savePromises.push(
configStore.updateStream({
encoder: config.value.encoder_backend as any,
stun_server: config.value.stun_server || undefined,
turn_server: config.value.turn_server || undefined,
turn_username: config.value.turn_username || undefined,
turn_password: config.value.turn_password || undefined,
})
)
await configStore.updateVideo({
device: config.value.video_device || undefined,
format: config.value.video_format || undefined,
width: config.value.video_width,
height: config.value.video_height,
fps: toConfigFps(config.value.video_fps),
})
await configStore.updateStream({
encoder: config.value.encoder_backend as any,
stun_server: config.value.stun_server || undefined,
turn_server: config.value.turn_server || undefined,
turn_username: config.value.turn_username || undefined,
turn_password: config.value.turn_password || undefined,
})
}
// HID config
// HID config (includes MSD enable — same gadget; must not race with updateHid)
if (activeSection.value === 'hid') {
if (!isHidFunctionSelectionValid.value || !isOtgEndpointBudgetValid.value) {
return
@@ -1024,24 +1020,20 @@ async function saveConfig() {
hidUpdate.otg_functions = { ...config.value.hid_otg_functions }
hidUpdate.otg_keyboard_leds = config.value.hid_otg_keyboard_leds
}
savePromises.push(configStore.updateHid(hidUpdate))
savePromises.push(
configStore.updateMsd({
enabled: config.value.msd_enabled,
})
)
await configStore.updateHid(hidUpdate)
await configStore.updateMsd({
enabled: config.value.msd_enabled,
})
}
// MSD config
if (activeSection.value === 'msd') {
savePromises.push(
configStore.updateMsd({
msd_dir: config.value.msd_dir || undefined,
})
)
await configStore.updateMsd({
msd_dir: config.value.msd_dir || undefined,
})
}
await Promise.all(savePromises)
await loadConfig()
saved.value = true
setTimeout(() => (saved.value = false), 2000)
} catch (e) {