fix:完成实时流检测联调

zlx
zhoulexin 13 hours ago
parent 4bbb398c46
commit f9e89d5bf9

@ -1,22 +1,16 @@
<template> <template>
<div class="webrtc-wrapper"> <div class="webrtc-wrapper">
<video <video
ref="videoRef" ref="videoRef"
autoplay autoplay
muted muted
playsinline playsinline
class="video-player" class="video-player"
></video> ></video>
<!-- 加载状态 -->
<div v-if="loading" class="loading-overlay"> <div v-if="loading" class="loading-overlay">
<span class="spinner"></span> <span class="spinner"></span>
<span>连接中...</span> <span>{{ loadingText }}</span>
</div>
<!-- 录制状态指示器 -->
<div v-if="isRecording" class="recording-indicator">
<span class="dot"></span> 录制中 {{ recordTime }}s
</div> </div>
<div v-if="error" class="error-overlay">{{ error }}</div> <div v-if="error" class="error-overlay">{{ error }}</div>
@ -24,206 +18,195 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'; import { ref, onMounted, onUnmounted, watch } from 'vue'
const props = defineProps({ const props = defineProps({
src: { type: String, required: true } src: { type: String, required: true }
}); })
const emit = defineEmits(['connection-status'])
const videoRef = ref(null)
const error = ref('')
const loading = ref(false)
const loadingText = ref('连接中...')
let pc = null
let sessionUrl = ''
let queuedCandidates = []
let offerData = null
let retryTimer = null
let retryCount = 0
const MAX_RETRIES = 30
const RETRY_INTERVAL = 2000
let destroyed = false
const whepUrl = () => {
const base = props.src.replace(/\/+$/, '')
return base + '/whep'
}
// src function parseOffer(sdp) {
watch(() => props.src, (newSrc, oldSrc) => { const ret = { iceUfrag: '', icePwd: '', medias: [] }
if (newSrc && newSrc !== oldSrc) { for (const line of sdp.split('\r\n')) {
disconnect(); if (line.startsWith('m=')) ret.medias.push(line.slice('m='.length))
connect(); else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) ret.iceUfrag = line.slice('a=ice-ufrag:'.length)
else if (ret.icePwd === '' && line.startsWith('a=ice-pwd:')) ret.icePwd = line.slice('a=ice-pwd:'.length)
} }
}); return ret
}
//
const emit = defineEmits(['record-status-change','record-complete','connection-status']); function generateSdpFragment(od, candidates) {
const byMedia = {}
const videoRef = ref(null); for (const c of candidates) {
const error = ref(''); const mid = c.sdpMLineIndex
const loading = ref(false); if (!byMedia[mid]) byMedia[mid] = []
let pc = null; byMedia[mid].push(c)
// --- ---
const isRecording = ref(false);
const recordTime = ref(0);
let mediaRecorder = null;
let recordedChunks = [];
let timerInterval = null;
// 1. WebRTC
const disconnect = () => {
if (pc) {
pc.close();
pc = null;
} }
if (videoRef.value) { let frag = 'a=ice-ufrag:' + od.iceUfrag + '\r\n' + 'a=ice-pwd:' + od.icePwd + '\r\n'
videoRef.value.srcObject = null; let mid = 0
for (const media of od.medias) {
if (byMedia[mid]) {
frag += 'm=' + media + '\r\n' + 'a=mid:' + mid + '\r\n'
for (const c of byMedia[mid]) frag += 'a=' + c.candidate + '\r\n'
}
mid++
} }
error.value = ''; return frag
}; }
function linkToIceServers(links) {
if (!links) return []
return links.split(', ').map((link) => {
const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i)
const ret = { urls: [m[1]] }
if (m[3] !== undefined) {
ret.username = JSON.parse(`"${m[3]}"`)
ret.credential = JSON.parse(`"${m[4]}"`)
ret.credentialType = 'password'
}
return ret
})
}
function sendLocalCandidates(candidates) {
fetch(sessionUrl, {
method: 'PATCH',
headers: { 'Content-Type': 'application/trickle-ice-sdpfrag', 'If-Match': '*' },
body: generateSdpFragment(offerData, candidates)
}).catch(() => {})
}
function cleanupConnection() {
if (pc) { pc.close(); pc = null }
if (videoRef.value) videoRef.value.srcObject = null
sessionUrl = ''
queuedCandidates = []
offerData = null
}
function cancelRetry() {
if (retryTimer) {
clearTimeout(retryTimer)
retryTimer = null
}
}
async function connect() {
if (!props.src || destroyed) return
cancelRetry()
cleanupConnection()
loading.value = true
loadingText.value = retryCount > 0 ? `连接中... (第 ${retryCount} 次重试)` : '连接中...'
error.value = ''
// 2. WebRTC
const connect = async () => {
loading.value = true;
error.value = '';
try { try {
pc = new RTCPeerConnection(); const optionsRes = await fetch(whepUrl(), { method: 'OPTIONS' })
const iceServers = linkToIceServers(optionsRes.headers.get('Link'))
pc = new RTCPeerConnection({ iceServers, sdpSemantics: 'unified-plan' })
pc.addTransceiver('video', { direction: 'sendrecv' })
pc.addTransceiver('audio', { direction: 'sendrecv' })
pc.ontrack = (event) => { pc.ontrack = (event) => {
if (videoRef.value && event.streams[0]) { if (videoRef.value && event.streams[0]) {
videoRef.value.srcObject = event.streams[0]; videoRef.value.srcObject = event.streams[0]
emit('connection-status', true); emit('connection-status', true)
retryCount = 0
loading.value = false
error.value = ''
}
}
pc.onicecandidate = (evt) => {
if (evt.candidate !== null) {
if (sessionUrl) sendLocalCandidates([evt.candidate])
else queuedCandidates.push(evt.candidate)
} }
}; }
pc.addTransceiver('video', { direction: 'recvonly' });
pc.addTransceiver('audio', { direction: 'recvonly' });
const offer = await pc.createOffer(); const offer = await pc.createOffer()
await pc.setLocalDescription(offer); offerData = parseOffer(offer.sdp)
await pc.setLocalDescription(offer)
const res = await fetch(props.src, { const res = await fetch(whepUrl(), {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/sdp' }, headers: { 'Content-Type': 'application/sdp' },
body: pc.localDescription.sdp body: offer.sdp
}); })
if (!res.ok) throw new Error(`Status: ${res.status}`); if (!res.ok) {
const errBody = await res.text().catch(() => '')
const answer = await res.text(); const isNotPublishing = errBody.includes('no one is publishing')
await pc.setRemoteDescription({ type: 'answer', sdp: answer }); if (isNotPublishing) throw new Error('NOT_PUBLISHING')
} catch (e) { throw new Error(errBody || `HTTP ${res.status}`)
error.value = '连接失败,请检查视频地址';
emit('connection-status', false);
console.error(e);
} finally {
let timeout = setTimeout(() => {
loading.value = false;
clearTimeout(timeout);
}, 1500);
}
};
// 2.
const takeSnapshot = () => {
if (!videoRef.value) return null;
const video = videoRef.value;
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// Blob (: image/png, image/jpeg)
return new Promise((resolve) => {
canvas.toBlob((blob) => {
resolve(blob);
}, 'image/jpeg', 0.9);
});
};
// 3.
const startRecord = () => {
if (!videoRef.value || isRecording.value) return;
try {
// MediaStream
// captureStream API Chrome/Edge/Firefox
const stream = videoRef.value.captureStream ? videoRef.value.captureStream() : videoRef.value.mozCaptureStream();
if (!stream) {
error.value = '浏览器不支持捕获视频流';
return;
} }
recordedChunks = []; const location = res.headers.get('location')
// MediaRecorder if (location) sessionUrl = new URL(location, whepUrl()).toString()
// mimeType 'video/webm; codecs=vp9'
mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
mediaRecorder.ondataavailable = (event) => { const answer = await res.text()
if (event.data.size > 0) { await pc.setRemoteDescription({ type: 'answer', sdp: answer })
recordedChunks.push(event.data);
} if (queuedCandidates.length > 0 && sessionUrl) {
}; sendLocalCandidates(queuedCandidates)
queuedCandidates = []
mediaRecorder.onstop = () => { }
const blob = new Blob(recordedChunks, { type: 'video/webm' });
recordedChunks = []; // retryCount = 0
// blob
emit('record-complete', blob);
console.log('录制完成Blob大小:', blob.size);
};
mediaRecorder.start();
isRecording.value = true;
recordTime.value = 0;
//
timerInterval = setInterval(() => {
recordTime.value++;
}, 1000);
emit('record-status-change', true);
} catch (err) {
console.error('录制启动失败:', err);
error.value = '录制启动失败';
}
};
// 4.
const stopRecord = () => {
if (mediaRecorder && isRecording.value) {
mediaRecorder.stop();
isRecording.value = false;
clearInterval(timerInterval);
emit('record-status-change', false);
}
};
// 5.
const handleRecordComplete = (blob) => {
console.log('录制完成,文件大小:', blob.size, 'bytes');
//
// uploadVideoToServer(blob);
// URL
// const url = URL.createObjectURL(blob);
};
//
const uploadVideoToServer = async (blob) => {
const formData = new FormData();
formData.append('file', blob, `record_${new Date().getTime()}.webm`);
try {
//
// await axios.post('/api/upload/video', formData);
console.log('视频上传成功');
} catch (e) { } catch (e) {
console.error('上传失败', e); const isNotPublishing = e.message === 'NOT_PUBLISHING'
cleanupConnection()
if (isNotPublishing && retryCount < MAX_RETRIES && !destroyed) {
retryCount++
loadingText.value = `等待推流中... (${retryCount}/${MAX_RETRIES})`
retryTimer = setTimeout(() => connect(), RETRY_INTERVAL)
} else {
error.value = isNotPublishing ? '视频流尚未就绪,请稍后重试' : '连接失败,请检查视频地址'
emit('connection-status', false)
loading.value = false
console.error('[WebRTC]', e.message === 'NOT_PUBLISHING' ? 'stream not publishing' : e)
}
} }
}; }
onMounted(() => connect()); watch(() => props.src, (newSrc, oldSrc) => {
if (newSrc && newSrc !== oldSrc) {
retryCount = 0
cancelRetry()
connect()
}
})
onMounted(() => connect())
onUnmounted(() => { onUnmounted(() => {
if (pc) pc.close(); destroyed = true
if (isRecording.value) stopRecord(); cancelRetry()
}); cleanupConnection()
})
//
defineExpose({
takeSnapshot,
startRecord,
stopRecord
});
</script> </script>
<style scoped> <style scoped>
@ -253,30 +236,4 @@ defineExpose({
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
</style>
.recording-indicator {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
display: flex;
align-items: center;
gap: 6px;
}
.dot {
width: 8px;
height: 8px;
background-color: red;
border-radius: 50%;
animation: blink 1s infinite;
}
@keyframes blink {
0% { opacity: 1; }
50% { opacity: 0.4; }
100% { opacity: 1; }
}
</style>

@ -12,7 +12,7 @@
:type="algorithmRunning ? 'danger' : 'success'" :type="algorithmRunning ? 'danger' : 'success'"
size="default" size="default"
@click="$emit('toggleAlgorithm')" @click="$emit('toggleAlgorithm')"
:disabled="offlineVideoMissing || isLiveMode" :disabled="!isLiveMode && offlineVideoMissing"
> >
{{ algorithmRunning ? '关闭算法' : '启动算法' }} {{ algorithmRunning ? '关闭算法' : '启动算法' }}
</el-button> </el-button>

@ -4,7 +4,6 @@
<WebRtcPlayer <WebRtcPlayer
v-if="videoMode === 'live'" v-if="videoMode === 'live'"
:src="webrtcUrl" :src="webrtcUrl"
@connection-status="$emit('connectionChange', $event)"
/> />
<!-- 离线视频模式 --> <!-- 离线视频模式 -->
<div v-else-if="videoMode === 'offline'" class="offline-player"> <div v-else-if="videoMode === 'offline'" class="offline-player">

@ -35,7 +35,6 @@
:webrtc-url="webrtcUrl" :webrtc-url="webrtcUrl"
:offline-video-url="offlineVideoUrl" :offline-video-url="offlineVideoUrl"
:video-autoplay-trigger="videoAutoplayTrigger" :video-autoplay-trigger="videoAutoplayTrigger"
@connection-change="onConnectionChange"
/> />
<DetectionProgress <DetectionProgress
@ -58,6 +57,21 @@
<div class="right-section"> <div class="right-section">
<ChartsPanel :process-stats="processStats" :counts="detectCounts" /> <ChartsPanel :process-stats="processStats" :counts="detectCounts" />
</div> </div>
<!-- SOP 未完成弹窗 -->
<el-dialog
v-model="sopAlert.visible"
:title="sopAlert.title"
width="500px"
:close-on-click-modal="false"
:close-on-press-escape="false"
draggable
>
<div style="white-space: pre-line; line-height: 1.8;">{{ sopAlert.content }}</div>
<template #footer>
<el-button type="primary" @click="sopAlert.visible = false">知道了</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
@ -78,26 +92,328 @@ import { downloadAsZip } from '@/utils/zip.js'
// ============ ============ // ============ ============
const videoMode = ref('idle') const videoMode = ref('idle')
const streamAddress = ref('') const streamAddress = ref('rtsp://10.23.22.97:8556/camera2')
const webrtcUrl = computed(() => { const webrtcUrl = ref('')
return import.meta.env.VITE_WEBRTC_URL || `http://${streamAddress.value}/stream`
})
const streamRunning = ref(false) const streamRunning = ref(false)
const isLiveMode = computed(() => videoMode.value === 'live') const isLiveMode = computed(() => videoMode.value === 'live')
const RAW_WS_URL = 'ws://10.23.22.43:8001/ws/raw_rtsp'
const DETECT_WS_URL = 'ws://10.23.22.43:8001/ws/detect_rtsp'
let rawWsClient = null
let detectWsClient = null
function destroyRawWs() {
if (rawWsClient) {
rawWsClient.destroy()
rawWsClient = null
}
}
function destroyDetectWs() {
if (detectWsClient) {
detectWsClient.destroy()
detectWsClient = null
}
}
function stopRawStream() {
if (rawWsClient && rawWsClient.getReadyState() === WebSocket.OPEN) {
rawWsClient.send({ type: 'stop' })
}
destroyRawWs()
}
let detectStopCallback = null
function stopDetectStream(onDone) {
if (detectWsClient && detectWsClient.getReadyState() === WebSocket.OPEN) {
detectStopCallback = onDone || null
detectWsClient.send({ type: 'stop' })
addLog('已发送停止请求,等待检测结果...', 'info')
} else {
//
if (onDone) onDone()
}
}
function buildRtspUrl(input) {
return input.startsWith('rtsp://') ? input : `rtsp://${input}`
}
function connectRawStream() {
destroyRawWs()
webrtcUrl.value = ''
addLog('正在连接原始流服务...', 'info')
rawWsClient = new WebSocketClient({
url: RAW_WS_URL,
autoReconnect: false,
heartbeatInterval: 0,
heartbeatTimeout: 0,
debug: true
})
rawWsClient.on('open', () => {
addLog('原始流服务已连接,请求流地址', 'info')
rawWsClient.send({ type: 'start_raw' })
})
rawWsClient.on('message', (msg) => {
switch (msg.type) {
case 'ready':
addLog(msg.message || '原始流服务就绪', 'info')
break
case 'processing_started':
addLog(msg.message || '原始流发布已启动', 'info')
break
case 'webrtc_stream':
webrtcUrl.value = msg.webrtc_url
addLog('已获取原始 WebRTC 流,开始播放', 'success')
ElMessage.success('原始视频流已连接')
break
case 'stopping':
addLog(msg.message || '原始流正在关闭', 'warning')
break
case 'error':
addLog('流服务错误: ' + (msg.message || ''), 'error')
ElMessage.error(msg.message || '流服务出错')
break
}
})
rawWsClient.on('close', () => {
addLog('原始流连接已关闭', 'warning')
if (videoMode.value === 'live' && !algorithmRunning.value) {
streamRunning.value = false
videoMode.value = 'idle'
}
})
rawWsClient.on('error', () => {
addLog('原始流连接失败', 'error')
ElMessage.error('连接原始流服务失败')
if (!algorithmRunning.value) {
videoMode.value = 'idle'
streamRunning.value = false
}
})
rawWsClient.connect()
}
function connectDetectStream() {
destroyDetectWs()
webrtcUrl.value = ''
addLog('正在连接检测流服务...', 'info')
// : 0-, 1-switch, 2-start_rtsp
let handshakeStep = 0
detectWsClient = new WebSocketClient({
url: DETECT_WS_URL,
autoReconnect: false,
heartbeatInterval: 0,
heartbeatTimeout: 0,
debug: true
})
detectWsClient.on('open', () => {
addLog('检测流服务已连接,开启算法开关...', 'info')
handshakeStep = 1
detectWsClient.send({ type: 'switch', enabled: true })
})
detectWsClient.on('message', (msg) => {
switch (msg.type) {
case 'ready':
addLog(msg.message || '检测流服务就绪', 'info')
// open switch ready ,
break
case 'switch':
if (handshakeStep === 1) {
// switch start_rtsp
addLog('算法开关已开启,发送 RTSP 地址', 'info')
handshakeStep = 2
detectWsClient.send({
type: 'start_rtsp',
rtsp_url: buildRtspUrl(streamAddress.value),
options: {
conf: 0.6,
iou: 0.45,
skip: 5,
imgsz: 640,
save_video: true,
publish_webrtc: true,
publish_fps: 25
}
})
}
break
case 'processing_started':
addLog(msg.message || 'RTSP 检测已启动', 'success')
break
case 'webrtc_stream':
webrtcUrl.value = msg.webrtc_url
addLog('已获取检测 WebRTC 流,开始播放', 'success')
ElMessage.success('检测流已连接')
break
case 'pause':
paused.value = Boolean(msg.paused)
addLog(msg.message || (paused.value ? '检测流已暂停' : '检测流已恢复'), 'warning')
break
case 'counts':
mergeCounts(msg.counts || [])
break
case 'progress':
if (msg.frame) {
const progress = msg.total_frames > 0
? (msg.frame / msg.total_frames * 100)
: 0
detectProgress.value = Math.round(progress)
detectStatusText.value = msg.is_rtsp
? `处理中 (第 ${msg.frame} 帧)`
: `处理中 (${msg.frame}/${msg.total_frames || '?'})`
}
break
case 'log':
addLog(msg.message || '', 'info')
break
case 'process_status':
addLog(msg.message || '', 'warning')
//
processStats.completed += msg.is_complete ? 1 : 0
processStats.incomplete += msg.is_complete ? 0 : 1
if (msg.should_alert || !msg.is_complete) {
const detail = (msg.missing_parts || [])
.map(item => `步骤${item.step_num} ${item.step_name}${item.part_name}`)
.join('\n')
Object.assign(sopAlert, {
visible: true,
title: `SOP 流程 #${msg.process_id} 未完成`,
content: detail
})
}
break
case 'stopping':
addLog(msg.message || '检测流正在关闭', 'warning')
break
case 'done':
addLog(msg.message || '检测完成', 'success')
// 线线
if (msg.output_video_url) {
const videoUrl = HTTP_BASE.replace(/\/$/, '') + msg.output_video_url
if (offlineVideoUrl.value) {
URL.revokeObjectURL(offlineVideoUrl.value)
}
offlineVideoUrl.value = videoUrl
videoMode.value = 'offline'
detectStatusText.value = '检测完成'
detectProgress.value = 100
addLog('检测完成,视频已更新', 'success')
// 线 done
const processes = msg.processes || []
processStats.completed = processes.filter(p => p.is_complete).length
processStats.incomplete = processes.filter(p => !p.is_complete).length
videoAutoplayTrigger.value++
exportVideoUrl.value = videoUrl
}
if (msg.report_url) {
exportReportUrl.value = HTTP_BASE.replace(/\/$/, '') + msg.report_url
}
if (msg.counts) mergeCounts(msg.counts)
if (exportVideoUrl.value || exportReportUrl.value) {
canExport.value = true
}
algorithmRunning.value = false
paused.value = false
destroyDetectWs()
//
if (detectStopCallback) {
const cb = detectStopCallback
detectStopCallback = null
cb()
}
break
case 'error':
addLog('检测流错误: ' + (msg.message || ''), 'error')
ElMessage.error(msg.message || '检测流出错')
algorithmRunning.value = false
destroyDetectWs()
connectRawStream()
break
}
})
detectWsClient.on('close', () => {
addLog('检测流连接已关闭', 'warning')
algorithmRunning.value = false
})
detectWsClient.on('error', () => {
addLog('检测流连接失败', 'error')
ElMessage.error('连接检测流服务失败')
algorithmRunning.value = false
})
detectWsClient.connect()
}
function switchStream() { function switchStream() {
if (!streamAddress.value) { if (!streamAddress.value) {
ElMessage.warning('请输入视频流地址') ElMessage.warning('请输入视频流地址')
return return
} }
stopRawStream()
videoMode.value = 'live' videoMode.value = 'live'
webrtcUrl.value = ''
algorithmRunning.value = false
paused.value = false
clearAllLiveData() clearAllLiveData()
stopDetectStream(() => connectRawStream())
} }
function toggleStream() { function toggleStream() {
streamRunning.value = !streamRunning.value
if (streamRunning.value) { if (streamRunning.value) {
// 线 //
stopRawStream()
if (algorithmRunning.value) {
// done
stopDetectStream(() => {
algorithmRunning.value = false
streamRunning.value = false
webrtcUrl.value = ''
paused.value = false
ending.value = false
ElMessage.info('视频流已关闭')
addLog('视频检测完成,已切换到结果视频', 'info')
})
} else {
stopDetectStream()
algorithmRunning.value = false
streamRunning.value = false
videoMode.value = 'idle'
webrtcUrl.value = ''
paused.value = false
ending.value = false
ElMessage.info('视频流已关闭')
addLog('视频流已关闭', 'info')
}
} else {
//
if (offlineVideoUrl.value) { if (offlineVideoUrl.value) {
URL.revokeObjectURL(offlineVideoUrl.value) URL.revokeObjectURL(offlineVideoUrl.value)
offlineVideoUrl.value = '' offlineVideoUrl.value = ''
@ -105,18 +421,23 @@ function toggleStream() {
resetDetectionState() resetDetectionState()
addLog('已清空离线视频文件,如需检测请重新上传', 'warning') addLog('已清空离线视频文件,如需检测请重新上传', 'warning')
} }
if (!streamAddress.value) {
ElMessage.warning('请先输入视频流地址')
return
}
streamRunning.value = true
videoMode.value = 'live' videoMode.value = 'live'
webrtcUrl.value = ''
clearAllLiveData() clearAllLiveData()
} else { // ""
videoMode.value = 'idle' if (rawWsClient) {
ElMessage.info('视频流已关闭') addLog('正在关闭原始流,切换到推算流...', 'info')
stopRawStream()
}
connectDetectStream()
} }
} }
function onConnectionChange(status) {
streamRunning.value = status
}
// ============ 线 ============ // ============ 线 ============
const offlineVideoUrl = ref('') const offlineVideoUrl = ref('')
const detectionFile = ref(null) const detectionFile = ref(null)
@ -153,6 +474,7 @@ const DEFAULT_COUNTS = [
] ]
const detectCounts = reactive(DEFAULT_COUNTS.map(c => ({ ...c }))) const detectCounts = reactive(DEFAULT_COUNTS.map(c => ({ ...c })))
const processStats = reactive({ completed: 0, incomplete: 0 }) const processStats = reactive({ completed: 0, incomplete: 0 })
const sopAlert = reactive({ visible: false, title: '', content: '' })
function mergeCounts(newCounts) { function mergeCounts(newCounts) {
newCounts.forEach(newItem => { newCounts.forEach(newItem => {
@ -232,6 +554,18 @@ function toggleAlgorithm() {
return return
} }
// done
if (algorithmRunning.value && videoMode.value === 'live') {
addLog('算法已关闭,等待检测结果...', 'info')
stopDetectStream(() => {
algorithmRunning.value = false
paused.value = false
ElMessage.info('算法已关闭')
addLog('检测结果已保存,切换到结果视频', 'info')
})
return
}
algorithmRunning.value = !algorithmRunning.value algorithmRunning.value = !algorithmRunning.value
if (algorithmRunning.value) { if (algorithmRunning.value) {
@ -243,7 +577,6 @@ function toggleAlgorithm() {
resetDetectionState() resetDetectionState()
detectStatusText.value = '正在连接 WebSocket...' detectStatusText.value = '正在连接 WebSocket...'
addLog('开始连接算法检测服务', 'info') addLog('开始连接算法检测服务', 'info')
//
videoAutoplayTrigger.value++ videoAutoplayTrigger.value++
wsClient = new WebSocketClient({ wsClient = new WebSocketClient({
@ -331,11 +664,9 @@ function toggleAlgorithm() {
detectStatusText.value = '检测完成' detectStatusText.value = '检测完成'
detectProgress.value = 100 detectProgress.value = 100
addLog('检测完成,视频已更新', 'success') addLog('检测完成,视频已更新', 'success')
//
const processes = msg.processes || [] const processes = msg.processes || []
processStats.completed = processes.filter(p => p.is_complete).length processStats.completed = processes.filter(p => p.is_complete).length
processStats.incomplete = processes.filter(p => !p.is_complete).length processStats.incomplete = processes.filter(p => !p.is_complete).length
//
mergeCounts(msg.counts || []) mergeCounts(msg.counts || [])
ElMessage.success('视频检测完成') ElMessage.success('视频检测完成')
algorithmRunning.value = false algorithmRunning.value = false
@ -343,7 +674,6 @@ function toggleAlgorithm() {
ending.value = false ending.value = false
wsClient.close() wsClient.close()
videoAutoplayTrigger.value++ videoAutoplayTrigger.value++
//
exportVideoUrl.value = videoUrl exportVideoUrl.value = videoUrl
exportReportUrl.value = msg.report_url ? (HTTP_BASE.replace(/\/$/, '') + msg.report_url) : '' exportReportUrl.value = msg.report_url ? (HTTP_BASE.replace(/\/$/, '') + msg.report_url) : ''
canExport.value = true canExport.value = true
@ -379,6 +709,23 @@ function toggleAlgorithm() {
}) })
wsClient.connect() wsClient.connect()
} else if (videoMode.value === 'live') {
// ""
algorithmRunning.value = true
paused.value = false
ending.value = false
flowLogs.value = []
resetDetectionState()
addLog('算法检测已启用', 'success')
if (!detectWsClient && !rawWsClient) {
//
connectDetectStream()
} else if (rawWsClient) {
//
addLog('正在关闭原始流,切换到检测流...', 'info')
stopRawStream()
connectDetectStream()
}
} else if (videoMode.value === 'offline' && !detectionFile.value) { } else if (videoMode.value === 'offline' && !detectionFile.value) {
ElMessage.warning('请先上传离线视频') ElMessage.warning('请先上传离线视频')
algorithmRunning.value = false algorithmRunning.value = false
@ -408,6 +755,22 @@ function toggleAlgorithm() {
} }
function togglePause() { function togglePause() {
if (isLiveMode.value && algorithmRunning.value) {
// WS
if (!detectWsClient || detectWsClient.getReadyState() !== WebSocket.OPEN) return
paused.value = !paused.value
detectWsClient.send({ type: 'pause', paused: paused.value })
addLog(paused.value ? '发送暂停请求...' : '发送恢复请求...', 'info')
return
}
if (isLiveMode.value) {
// WS
if (!rawWsClient || rawWsClient.getReadyState() !== WebSocket.OPEN) return
paused.value = !paused.value
rawWsClient.send({ type: 'pause', paused: paused.value })
addLog(paused.value ? '发送暂停请求...' : '发送恢复请求...', 'info')
return
}
if (!wsClient || wsClient.getReadyState() !== WebSocket.OPEN) return if (!wsClient || wsClient.getReadyState() !== WebSocket.OPEN) return
paused.value = !paused.value paused.value = !paused.value
wsClient.send({ type: 'pause', paused: paused.value }) wsClient.send({ type: 'pause', paused: paused.value })
@ -444,9 +807,12 @@ onUnmounted(() => {
URL.revokeObjectURL(offlineVideoUrl.value) URL.revokeObjectURL(offlineVideoUrl.value)
} }
if (wsClient) { if (wsClient) {
wsClient.send({ type: 'stop' })
wsClient.destroy() wsClient.destroy()
wsClient = null wsClient = null
} }
stopRawStream()
stopDetectStream()
}) })
</script> </script>

Loading…
Cancel
Save