feat:计算视频帧传来的差值

This commit is contained in:
nnbcccscdscdsc
2026-04-02 15:29:49 +08:00
parent 1e828cc036
commit 51ea86e887
6 changed files with 294 additions and 26 deletions

View File

@@ -1,37 +1,82 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { buildVideoFrameUrl } from '@/lib/api'
import { buildVideoFrameUrl, fetchVideoStatus } from '@/lib/api'
import type { VideoStatus } from '@/types'
const props = defineProps<{
video: VideoStatus | null
}>()
const STATUS_REFRESH_MS = 300
const liveVideo = ref<VideoStatus | null>(props.video)
const frameUrl = ref(buildVideoFrameUrl(0))
const currentFps = computed(() => props.video?.fps ?? 30)
const canRequestFrames = computed(() => props.video?.available === true)
const displayVideo = computed(() => liveVideo.value ?? props.video)
const currentFps = computed(() => displayVideo.value?.fps ?? 30)
const canRequestFrames = computed(() => displayVideo.value?.available === true)
const modeLabel = computed(() => {
if (!props.video) {
if (!displayVideo.value) {
return '正在获取视频状态'
}
if (props.video.source_mode === 'omnisocket-jpeg-live') {
return `${props.video.fps} FPS 实时接收`
if (displayVideo.value.source_mode === 'omnisocket-jpeg-live') {
return `${displayVideo.value.fps} FPS 实时接收`
}
if (props.video.source_mode === 'omnisocket-waiting') {
if (displayVideo.value.source_mode === 'omnisocket-waiting') {
return '未实时获取真实值'
}
return `${props.video.fps} FPS`
return `${displayVideo.value.fps} FPS`
})
const placeholderText = computed(() => {
if (!props.video) {
if (!displayVideo.value) {
return '正在获取视频状态...'
}
return '未实时获取真实值'
})
const latencyLabels = computed(() => {
const sampleWindowSize = displayVideo.value?.timing?.sample_window_size ?? 10
const samples = displayVideo.value?.timing?.delta_samples_ms ?? []
return Array.from({ length: sampleWindowSize }, (_, index) => samples[index] ?? null)
})
const timingHeadline = computed(() => {
const latest = displayVideo.value?.timing?.latest_delta_ms
if (latest == null) {
return '等待帧尾时间'
}
return `最新 ${latest.toFixed(1)} ms`
})
const timingHint = computed(() => {
const timing = displayVideo.value?.timing
if (!timing?.available) {
return '当前还没有从 JPEG 结尾后的尾字节里解析到时间戳,标签会在收到有效帧尾时间后自动填充。'
}
const unitText = timing.timestamp_unit ? `,单位按 ${timing.timestamp_unit}` : ''
const endiannessText = timing.timestamp_endianness
? `,字节序按 ${timing.timestamp_endianness}`
: ''
return `最近 ${timing.sample_window_size} 个差值样本,面板按 ${STATUS_REFRESH_MS} ms 刷新一组${unitText}${endiannessText}`
})
let frameTimer: number | null = null
let statusTimer: number | null = null
let frameKey = 0
let statusRequestPending = false
async function refreshStatus() {
if (statusRequestPending) {
return
}
statusRequestPending = true
try {
liveVideo.value = await fetchVideoStatus()
} catch {
// 保持当前已显示状态,避免短暂请求失败把面板内容清空。
} finally {
statusRequestPending = false
}
}
function refreshFrame() {
if (!canRequestFrames.value) {
@@ -58,7 +103,20 @@ function startFrameLoop() {
}, intervalMs)
}
function startStatusLoop() {
if (statusTimer != null) {
window.clearInterval(statusTimer)
statusTimer = null
}
void refreshStatus()
statusTimer = window.setInterval(() => {
void refreshStatus()
}, STATUS_REFRESH_MS)
}
onMounted(() => {
startStatusLoop()
startFrameLoop()
})
@@ -66,8 +124,19 @@ onUnmounted(() => {
if (frameTimer != null) {
window.clearInterval(frameTimer)
}
if (statusTimer != null) {
window.clearInterval(statusTimer)
}
})
watch(
() => props.video,
(nextVideo) => {
liveVideo.value = nextVideo
},
{ immediate: true },
)
watch([currentFps, canRequestFrames], () => {
startFrameLoop()
})
@@ -80,8 +149,8 @@ watch([currentFps, canRequestFrames], () => {
<p class="eyebrow">Video</p>
<h2>JPEG 视频流</h2>
</div>
<span class="badge" :class="{ bad: !video?.available }">
{{ video?.source_mode ?? 'loading' }}
<span class="badge" :class="{ bad: !displayVideo?.available }">
{{ displayVideo?.source_mode ?? 'loading' }}
</span>
</div>
@@ -100,7 +169,7 @@ watch([currentFps, canRequestFrames], () => {
<div class="stats">
<div class="stat-card">
<span>帧源</span>
<strong>{{ video?.frame_count ?? '--' }} JPEG</strong>
<strong>{{ displayVideo?.frame_count ?? '--' }} JPEG</strong>
</div>
<div class="stat-card">
<span>当前模式</span>
@@ -108,13 +177,33 @@ watch([currentFps, canRequestFrames], () => {
</div>
</div>
<div class="timing-panel">
<div class="timing-head">
<span>帧尾时间差</span>
<strong>{{ timingHeadline }}</strong>
</div>
<div class="timing-grid">
<span
v-for="(sample, index) in latencyLabels"
:key="index"
class="timing-label"
:class="{ empty: sample == null }"
>
{{ sample == null ? '--' : `${sample.toFixed(1)} ms` }}
</span>
</div>
<p class="hint subtle">
{{ timingHint }}
</p>
</div>
<p class="hint">
这里只有在后端已经收到 OmniSocket 的真实 JPEG 帧时才会开始逐帧请求并显示画面
如果当前没有真实帧页面会保持占位提示不再回退测试视频流
</p>
<p class="hint subtle">
当前帧源状态{{ video?.source_detail ?? '暂无' }}
当前帧源状态{{ displayVideo?.source_detail ?? '暂无' }}
</p>
</section>
</template>
@@ -213,6 +302,57 @@ h2 {
font-size: 18px;
}
.timing-panel {
display: grid;
gap: 12px;
padding: 14px;
border-radius: 18px;
background: rgba(7, 14, 26, 0.88);
border: 1px solid rgba(133, 147, 169, 0.18);
}
.timing-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
color: #cfd7e6;
}
.timing-head span {
color: #8d99b3;
font-size: 12px;
}
.timing-head strong {
font-size: 16px;
}
.timing-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 8px;
}
.timing-label {
display: grid;
place-items: center;
min-height: 40px;
padding: 8px 10px;
border-radius: 12px;
background: rgba(91, 122, 255, 0.12);
border: 1px solid rgba(91, 122, 255, 0.28);
color: #dce4ff;
font-size: 12px;
font-weight: 700;
}
.timing-label.empty {
background: rgba(133, 147, 169, 0.08);
border-color: rgba(133, 147, 169, 0.18);
color: #7e8aa5;
}
.hint {
margin: 0;
color: #8d99b3;
@@ -227,5 +367,9 @@ h2 {
.stats {
grid-template-columns: 1fr;
}
.timing-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>