@@ -1,76 +1,53 @@
< script setup lang = "ts" >
import { computed , onMounted , onUnmounted , ref , watch } from 'vue'
import { computed , ref , watch } from 'vue'
import { buildVideoFr ame Url } from '@/lib/api'
import { buildVideoStre amUrl } from '@/lib/api'
import type { VideoStatus } from '@/types'
const props = defineProps < {
video : VideoStatus | null
} > ( )
const fr ame Url = ref ( buildVideoFrameUrl ( 0 ) )
const currentFps = computed ( ( ) => props . video ? . fps ? ? 30 )
const stre amUrl = ref ( '' )
const canRequestFrames = computed ( ( ) => props . video ? . available === true )
const modeLabel = computed ( ( ) => {
const streamFps = computed ( ( ) => Math . max ( props . video ? . fps ? ? 0 , 30 ) )
const senderProfileLabel = computed ( ( ) => {
if ( ! props . video ) {
return '正在获取视频状态 '
return '发送端 Profile: -- '
}
if ( props . video . source _mode === 'omnisocket-jpeg-live' ) {
return ` ${ props . video . fps } FPS 实时接收 `
}
if ( props . video . source _mode === 'omnisocket-waiting' ) {
return '未实时获取真实值'
}
return ` ${ props . video . fps } FPS `
return ` 发送端 Profile: ${ props . video . fps } FPS `
} )
const displayModeLabel = computed ( ( ) => {
if ( ! props . video ) {
return '前端显示: 等待状态'
}
if ( ! canRequestFrames . value ) {
return '前端显示: 等待新视频帧'
}
return ` 前端显示: MJPEG Stream ( ${ streamFps . value } FPS 请求) `
} )
const placeholderText = computed ( ( ) => {
if ( ! props . video ) {
return '正在获取视频状态...'
}
return '未实时获取真实值 '
return '尚未收到实时视频帧 '
} )
let frameTimer : number | null = null
let frameKey = 0
function refreshFrame ( ) {
if ( ! canRequestFrames . value ) {
return
}
frameKey += 1
frameUrl . value = buildVideoFrameUrl ( frameKey )
}
function startFrameLoop ( ) {
if ( frameTimer != null ) {
window . clearInterval ( frameTimer )
frameTimer = null
}
function refreshStreamUrl ( ) {
if ( ! canRequestFrames . value ) {
streamUrl . value = ''
return
}
refreshFrame ( )
const intervalMs = Math . max ( 33 , Math . round ( 1000 / currentFps . value ) )
frameTimer = window . setInterval ( ( ) => {
refreshFrame ( )
} , intervalMs )
// Keep the browser attached to a higher-frequency local MJPEG stream
// so the dashboard does not add an extra 0-100 ms polling delay.
streamUrl . value = buildVideoStreamUrl ( streamFps . value , Date . now ( ) )
}
onMounted ( ( ) => {
startFrameLoop ( )
} )
onUnmounted ( ( ) => {
if ( frameTimer != null ) {
window . clearInterval ( frameTimer )
}
} )
watch ( [ currentFps , canRequestFrames ] , ( ) => {
startFrameLoop ( )
} )
watch ( [ streamFps , canRequestFrames ] , refreshStreamUrl , { immediate : true } )
< / script >
< template >
@@ -89,7 +66,7 @@ watch([currentFps, canRequestFrames], () => {
< img
v-if = "canRequestFrames"
class = "video-frame"
:src = "fr ame Url"
:src = "stre amUrl"
alt = "Robot jpeg frame stream"
/ >
< div v-else class = "video-placeholder" >
@@ -104,13 +81,16 @@ watch([currentFps, canRequestFrames], () => {
< / div >
< div class = "stat-card" >
< span > 当前模式 < / span >
< strong > { { modeLabel } } < / strong >
< div class = "stat-lines" >
< strong > { { senderProfileLabel } } < / strong >
< strong class = "secondary" > { { displayModeLabel } } < / strong >
< / div >
< / div >
< / div >
< p class = "hint" >
这里只有在后端已经收到 OmniSocket 的真实 JPEG 帧时 , 才会开始逐帧请求并显示画面 。
如果当前没有真实帧 , 页面会保持占位提示 , 不再回退测试视频流 。
视频可用时 , 页面会直接连接后端的 M JPEG stream , 而不是按当前发送 fps 逐帧轮询 。
这样能减少 dashboard 本身带来的额外显示延迟 。
< / p >
< p class = "hint subtle" >
@@ -213,6 +193,16 @@ h2 {
font - size : 18 px ;
}
. stat - lines {
display : grid ;
gap : 6 px ;
}
. stat - lines . secondary {
font - size : 15 px ;
color : # a8b4ce ;
}
. hint {
margin : 0 ;
color : # 8 d99b3 ;