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