Files
robot-command-center/frontend/src/components/VideoPanel.vue
nnbcccscdscdsc 771829d99d first commit
2026-03-31 20:41:08 +08:00

229 lines
4.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { buildVideoFrameUrl } from '@/lib/api'
import type { VideoStatus } from '@/types'
const props = defineProps<{
video: VideoStatus | null
}>()
const frameUrl = ref(buildVideoFrameUrl(0))
const currentFps = computed(() => props.video?.fps ?? 30)
const canRequestFrames = computed(() => props.video == null || props.video.available)
const modeLabel = computed(() => {
if (!props.video) {
return '--'
}
if (props.video.source_mode === 'omnisocket-jpeg-live') {
return `${props.video.fps} FPS 实时接收`
}
if (props.video.source_mode === 'omnisocket-waiting') {
return '等待 OmniSocket 实时帧'
}
if (props.video.source_mode === 'sample-jpeg-frame-loop') {
return `${props.video.fps} FPS 本地演示`
}
return `${props.video.fps} FPS`
})
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
}
if (!canRequestFrames.value) {
return
}
refreshFrame()
const intervalMs = Math.max(33, Math.round(1000 / currentFps.value))
frameTimer = window.setInterval(() => {
refreshFrame()
}, intervalMs)
}
onMounted(() => {
startFrameLoop()
})
onUnmounted(() => {
if (frameTimer != null) {
window.clearInterval(frameTimer)
}
})
watch([currentFps, canRequestFrames], () => {
startFrameLoop()
})
</script>
<template>
<section class="panel video-panel">
<div class="panel-head">
<div>
<p class="eyebrow">Video</p>
<h2>JPEG 视频流</h2>
</div>
<span class="badge" :class="{ bad: !video?.available }">
{{ video?.source_mode ?? 'loading' }}
</span>
</div>
<div class="video-shell">
<img
v-if="canRequestFrames"
class="video-frame"
:src="frameUrl"
alt="Robot jpeg frame stream"
/>
<div v-else class="video-placeholder">
正在等待 OmniSocket 实时 JPEG 帧接入...
</div>
</div>
<div class="stats">
<div class="stat-card">
<span>帧源</span>
<strong>{{ video?.frame_count ?? '--' }} JPEG</strong>
</div>
<div class="stat-card">
<span>当前模式</span>
<strong>{{ modeLabel }}</strong>
</div>
</div>
<p class="hint">
这里始终按固定频率逐张请求 Django 返回的单帧 JPEG不依赖 MJPEG只要后端已经收到
OmniSocket 里的真实 JPEG 这个组件就会直接显示实时画面
</p>
<p class="hint subtle">
当前帧源状态{{ video?.source_detail ?? '暂无' }}
</p>
</section>
</template>
<style scoped>
.video-panel {
display: grid;
gap: 16px;
}
.panel-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: start;
}
.eyebrow {
margin: 0 0 4px;
color: #5b7aff;
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 12px;
font-weight: 700;
}
h2 {
margin: 0;
font-size: 24px;
}
.badge {
padding: 8px 12px;
border-radius: 999px;
background: rgba(40, 199, 111, 0.16);
color: #63e6a9;
font-size: 12px;
font-weight: 700;
}
.badge.bad {
background: rgba(255, 107, 107, 0.18);
color: #ffb4b4;
}
.video-shell {
overflow: hidden;
border-radius: 20px;
border: 1px solid rgba(133, 147, 169, 0.28);
background: linear-gradient(180deg, #09111f 0%, #050812 100%);
}
.video-frame {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
background: #02050d;
}
.video-placeholder {
display: grid;
place-items: center;
width: 100%;
aspect-ratio: 16 / 9;
padding: 24px;
color: #a8b4ce;
text-align: center;
line-height: 1.7;
background:
radial-gradient(circle at top, rgba(91, 122, 255, 0.14), transparent 42%),
#02050d;
}
.stats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.stat-card {
padding: 14px;
border-radius: 16px;
background: rgba(7, 14, 26, 0.78);
border: 1px solid rgba(133, 147, 169, 0.2);
}
.stat-card span {
display: block;
margin-bottom: 8px;
color: #8d99b3;
font-size: 12px;
}
.stat-card strong {
font-size: 18px;
}
.hint {
margin: 0;
color: #8d99b3;
line-height: 1.65;
}
.hint.subtle {
font-size: 13px;
}
@media (max-width: 720px) {
.stats {
grid-template-columns: 1fr;
}
}
</style>