fix: 修复无信号状态同步与采集管线阻塞

- 使用非阻塞 V4L2 句柄并类型化采集错误
- 完善视频管线生命周期,避免复用停止中的管线
- 区分音视频事件并支持 REST/WebSocket 状态快照
- 修复信号丢失后保留最后一帧及刷新后黑屏问题
- 优化无信号提示、状态展示优先级及相关界面细节
- 降低重复 OTG 错误日志级别
This commit is contained in:
mofeng-git
2026-07-30 22:43:59 +08:00
parent ce1712ff2e
commit 6fdcf5c7c9
24 changed files with 406 additions and 165 deletions

View File

@@ -27,7 +27,7 @@ import { formatFpsValue } from '@/lib/fps'
import { videoDebugLog } from '@/lib/debugLog'
import { formatVideoDeviceLabel } from '@/lib/video-device-label'
import { isAudioDeviceLostStateReason, isAudioStreamDeviceLostPayload } from '@/lib/streamSignal'
import type { StreamDeviceLostEventData } from '@/types/websocket'
import type { StreamDeviceLostEventData, StreamStateChangedEventData } from '@/types/websocket'
import type { VideoMode } from '@/components/VideoConfigPopover.vue'
import StatusCard, { type StatusDetail } from '@/components/StatusCard.vue'
@@ -1356,7 +1356,7 @@ function handleStreamModeSwitching(data: { transition_id: string; to_mode: strin
videoSession.onModeSwitching(data)
}
function handleStreamStateChanged(data: any) {
function handleStreamStateChanged(data: StreamStateChangedEventData) {
videoDebugLog('Stream state changed event', {
data,
videoMode: videoMode.value,
@@ -1468,8 +1468,8 @@ const signalOverlayInfo = computed(() => {
case 'no_signal':
return {
title: t('console.signal.noSignal.title'),
detail: t('console.signal.noSignal.detail'),
hint,
detail: hint || t('console.signal.noSignal.detail'),
hint: '',
tone: 'info' as const,
}
case 'device_lost':
@@ -3083,6 +3083,10 @@ onMounted(async () => {
}, { immediate: true })
await systemStore.startStream().catch(() => {})
const initialStreamStatus = await streamApi.status().catch(() => null)
if (initialStreamStatus) {
handleStreamStateChanged(initialStreamStatus)
}
await systemStore.fetchSystemInfo().catch(() => {})
await systemStore.fetchAllStates()
await configStore.refreshHid().then(() => {
@@ -3398,7 +3402,7 @@ onUnmounted(() => {
</div>
<Transition name="fade">
<div
v-if="videoLoading"
v-if="videoLoading && !showSignalOverlay && !videoError"
class="absolute inset-0 flex flex-col items-center justify-center bg-black/70 backdrop-blur-sm transition-opacity duration-300"
>
<div class="absolute inset-0 overflow-hidden pointer-events-none">
@@ -3443,7 +3447,7 @@ onUnmounted(() => {
</Transition>
<Transition name="fade">
<div
v-if="showSignalOverlay && !videoLoading && !videoError"
v-if="showSignalOverlay && !videoError"
class="absolute inset-0 flex flex-col items-center justify-center gap-3 p-4 transition-opacity duration-300 pointer-events-none"
:class="{
'bg-black/80 backdrop-blur-sm': signalOverlayInfo.tone === 'error',

View File

@@ -2717,7 +2717,6 @@ watch(isWindows, () => {
<Sidebar class="top-10 h-[calc(100dvh-2.5rem)] sm:top-14 sm:h-[calc(100dvh-3.5rem)]" collapsible="offcanvas">
<SidebarHeader class="gap-1 px-6 pt-4 pb-2 text-foreground md:pt-6">
<h1 class="text-xl font-semibold">{{ t('settings.title') }}</h1>
<p class="text-xs text-muted-foreground">{{ t('settings.sidebarSubtitle') }}</p>
</SidebarHeader>
<SidebarContent class="px-3 pb-6 md:pb-10">
<SidebarGroup v-for="group in navGroups" :key="group.title" class="px-0 py-1">