From 2d81a071e5a36aa0fdec3aabcb8f6788b8f7e8ac Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Sat, 11 Apr 2026 15:11:47 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20MJPEG=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=8F=AF=E7=94=A8=E4=BD=86=E7=8A=B6=E6=80=81=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E7=A6=BB=E7=BA=BF=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + web/src/stores/system.ts | 27 ++++++++++++++++++++++++++- web/src/views/ConsoleView.vue | 14 ++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8b1bc2e4..ffbbda9c 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ CLAUDE.md secrets.toml .env /docs/ +web/package-lock.json diff --git a/web/src/stores/system.ts b/web/src/stores/system.ts index 4864db0f..38a80a8c 100644 --- a/web/src/stores/system.ts +++ b/web/src/stores/system.ts @@ -259,6 +259,30 @@ export const useSystemStore = defineStore('system', () => { } } + async function fetchStreamState() { + try { + const [status, modeResp] = await Promise.all([ + streamApi.status(), + streamApi.getMode().catch(() => ({ mode: 'mjpeg' })) + ]) + stream.value = { + online: status.state === 'streaming', + active: status.state !== 'uninitialized', + device: status.device, + format: status.format, + resolution: status.resolution, + targetFps: status.target_fps, + clients: status.clients, + streamMode: modeResp.mode || 'mjpeg', + error: status.state === 'error' ? 'Stream error' : null, + } + return status + } catch (e) { + console.error('Failed to fetch stream state:', e) + throw e + } + } + async function fetchAllStates() { loading.value = true error.value = null @@ -266,7 +290,8 @@ export const useSystemStore = defineStore('system', () => { try { await Promise.all([ fetchSystemInfo(), - // HID state is updated via WebSocket device_info event + fetchStreamState().catch(() => null), + fetchHidState().catch(() => null), fetchAtxState().catch(() => null), fetchMsdState().catch(() => null), ]) diff --git a/web/src/views/ConsoleView.vue b/web/src/views/ConsoleView.vue index f7462460..7ce6bf49 100644 --- a/web/src/views/ConsoleView.vue +++ b/web/src/views/ConsoleView.vue @@ -96,6 +96,7 @@ const videoLoading = ref(true) const videoError = ref(false) const videoErrorMessage = ref('') const videoRestarting = ref(false) // Track if video is restarting due to config change +const mjpegFrameReceived = ref(false) // Whether MJPEG stream has received at least one frame // Video aspect ratio (dynamically updated from actual video dimensions) // Using string format "width/height" to let browser handle the ratio calculation @@ -188,6 +189,11 @@ const videoStatus = computed<'connected' | 'connecting' | 'disconnected' | 'erro if (webrtc.isConnecting.value) return 'connecting' if (webrtc.isConnected.value) return 'connected' } + // MJPEG: check if frames have actually arrived (frontend-side detection) + // This is more reliable than relying on stream.online from backend, + // which can be stale due to the debounce delay in device_info broadcaster. + // Also handles browsers that don't fire img.onload for multipart MJPEG streams. + if (videoMode.value === 'mjpeg' && mjpegFrameReceived.value) return 'connected' if (systemStore.stream?.online) return 'connected' return 'disconnected' }) @@ -680,6 +686,7 @@ function handleVideoLoad() { // MJPEG video frame loaded successfully - update stream online status // This fixes the timing issue where device_info event may arrive before stream is fully active if (videoMode.value === 'mjpeg') { + mjpegFrameReceived.value = true systemStore.setStreamOnline(true) // Update aspect ratio from MJPEG image dimensions const img = videoRef.value @@ -758,6 +765,7 @@ function handleVideoError() { // Show loading state immediately videoLoading.value = true + mjpegFrameReceived.value = false // Auto-retry with exponential backoff (infinite retry, capped delay) retryCount++ @@ -1062,6 +1070,7 @@ function refreshVideo() { backendFps.value = 0 videoError.value = false videoErrorMessage.value = '' + mjpegFrameReceived.value = false // Update timestamp to force MJPEG reconnection via reactive URL isRefreshingVideo = true @@ -2061,6 +2070,11 @@ async function activateConsoleView() { isConsoleActive.value = true registerInteractionListeners() + // Ensure HID WebSocket is connected when console becomes active + if (!hidWs.connected.value) { + hidWs.connect().catch(() => {}) + } + if (videoMode.value !== 'mjpeg' && webrtc.videoTrack.value) { await nextTick() await rebindWebRTCVideo()