From f9e89d5bf90f7090959ddbf5f7d0f07a3a34bec0 Mon Sep 17 00:00:00 2001 From: zhoulexin <544279058@qq.com> Date: Mon, 29 Jun 2026 17:48:04 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E5=AE=8C=E6=88=90=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E6=B5=81=E6=A3=80=E6=B5=8B=E8=81=94=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/WebRtcPlayer.vue | 391 ++++++++---------- src/views/home/components/ControlButtons.vue | 2 +- src/views/home/components/VideoPlayer.vue | 1 - src/views/home/index.vue | 402 ++++++++++++++++++- 4 files changed, 559 insertions(+), 237 deletions(-) diff --git a/src/components/WebRtcPlayer.vue b/src/components/WebRtcPlayer.vue index 794d76c..08d684e 100644 --- a/src/components/WebRtcPlayer.vue +++ b/src/components/WebRtcPlayer.vue @@ -1,22 +1,16 @@ \ No newline at end of file + diff --git a/src/views/home/components/ControlButtons.vue b/src/views/home/components/ControlButtons.vue index 68513ca..7d28fb0 100644 --- a/src/views/home/components/ControlButtons.vue +++ b/src/views/home/components/ControlButtons.vue @@ -12,7 +12,7 @@ :type="algorithmRunning ? 'danger' : 'success'" size="default" @click="$emit('toggleAlgorithm')" - :disabled="offlineVideoMissing || isLiveMode" + :disabled="!isLiveMode && offlineVideoMissing" > {{ algorithmRunning ? '关闭算法' : '启动算法' }} diff --git a/src/views/home/components/VideoPlayer.vue b/src/views/home/components/VideoPlayer.vue index f4d8ef2..1130136 100644 --- a/src/views/home/components/VideoPlayer.vue +++ b/src/views/home/components/VideoPlayer.vue @@ -4,7 +4,6 @@
diff --git a/src/views/home/index.vue b/src/views/home/index.vue index 4cdd617..376520d 100644 --- a/src/views/home/index.vue +++ b/src/views/home/index.vue @@ -35,7 +35,6 @@ :webrtc-url="webrtcUrl" :offline-video-url="offlineVideoUrl" :video-autoplay-trigger="videoAutoplayTrigger" - @connection-change="onConnectionChange" />
+ + + +
{{ sopAlert.content }}
+ +
@@ -78,26 +92,328 @@ import { downloadAsZip } from '@/utils/zip.js' // ============ 视频流相关 ============ const videoMode = ref('idle') -const streamAddress = ref('') -const webrtcUrl = computed(() => { - return import.meta.env.VITE_WEBRTC_URL || `http://${streamAddress.value}/stream` -}) +const streamAddress = ref('rtsp://10.23.22.97:8556/camera2') +const webrtcUrl = ref('') const streamRunning = ref(false) 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() { if (!streamAddress.value) { ElMessage.warning('请输入视频流地址') return } + stopRawStream() videoMode.value = 'live' + webrtcUrl.value = '' + algorithmRunning.value = false + paused.value = false clearAllLiveData() + stopDetectStream(() => connectRawStream()) } function toggleStream() { - streamRunning.value = !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) { URL.revokeObjectURL(offlineVideoUrl.value) offlineVideoUrl.value = '' @@ -105,18 +421,23 @@ function toggleStream() { resetDetectionState() addLog('已清空离线视频文件,如需检测请重新上传', 'warning') } + if (!streamAddress.value) { + ElMessage.warning('请先输入视频流地址') + return + } + streamRunning.value = true videoMode.value = 'live' + webrtcUrl.value = '' clearAllLiveData() - } else { - videoMode.value = 'idle' - ElMessage.info('视频流已关闭') + // 如果原始流还在运行(由"切换"启动),先关闭 + if (rawWsClient) { + addLog('正在关闭原始流,切换到推算流...', 'info') + stopRawStream() + } + connectDetectStream() } } -function onConnectionChange(status) { - streamRunning.value = status -} - // ============ 离线视频相关 ============ const offlineVideoUrl = ref('') const detectionFile = ref(null) @@ -153,6 +474,7 @@ const DEFAULT_COUNTS = [ ] const detectCounts = reactive(DEFAULT_COUNTS.map(c => ({ ...c }))) const processStats = reactive({ completed: 0, incomplete: 0 }) +const sopAlert = reactive({ visible: false, title: '', content: '' }) function mergeCounts(newCounts) { newCounts.forEach(newItem => { @@ -232,6 +554,18 @@ function toggleAlgorithm() { 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 if (algorithmRunning.value) { @@ -243,7 +577,6 @@ function toggleAlgorithm() { resetDetectionState() detectStatusText.value = '正在连接 WebSocket...' addLog('开始连接算法检测服务', 'info') - // 触发视频自动播放 videoAutoplayTrigger.value++ wsClient = new WebSocketClient({ @@ -331,11 +664,9 @@ function toggleAlgorithm() { detectStatusText.value = '检测完成' detectProgress.value = 100 addLog('检测完成,视频已更新', 'success') - // 更新流程统计(饼图数据) const processes = msg.processes || [] processStats.completed = processes.filter(p => p.is_complete).length processStats.incomplete = processes.filter(p => !p.is_complete).length - // 更新检测计数(柱状图数据) mergeCounts(msg.counts || []) ElMessage.success('视频检测完成') algorithmRunning.value = false @@ -343,7 +674,6 @@ function toggleAlgorithm() { ending.value = false wsClient.close() videoAutoplayTrigger.value++ - // 保存导出数据 exportVideoUrl.value = videoUrl exportReportUrl.value = msg.report_url ? (HTTP_BASE.replace(/\/$/, '') + msg.report_url) : '' canExport.value = true @@ -379,6 +709,23 @@ function toggleAlgorithm() { }) 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) { ElMessage.warning('请先上传离线视频') algorithmRunning.value = false @@ -408,6 +755,22 @@ function toggleAlgorithm() { } 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 paused.value = !paused.value wsClient.send({ type: 'pause', paused: paused.value }) @@ -444,9 +807,12 @@ onUnmounted(() => { URL.revokeObjectURL(offlineVideoUrl.value) } if (wsClient) { + wsClient.send({ type: 'stop' }) wsClient.destroy() wsClient = null } + stopRawStream() + stopDetectStream() })