From e501d8095340adebe8f177ded99bdc4a6f2d6f93 Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Mon, 3 Aug 2026 14:43:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9C=BA=E6=A2=B0=E8=87=82=E6=8F=90?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/websocket.js | 147 +++-- .../register/components/Camera/index.vue | 582 ++++++++++++------ .../components/params/BasicNodeParams.vue | 16 +- 3 files changed, 500 insertions(+), 245 deletions(-) diff --git a/src/plugins/websocket.js b/src/plugins/websocket.js index 74de402..f3ca635 100644 --- a/src/plugins/websocket.js +++ b/src/plugins/websocket.js @@ -1,4 +1,8 @@ -import { throttle } from 'lodash'; +const MEDIA_HEADER_SIZE = 26; +const MEDIA_MAGIC = [0x43, 0x4d, 0x56, 0x46]; // CMVF +const MEDIA_MIME_TYPES = { + 1: 'video/mp4; codecs="avc1.42C01E"' +}; class WebSocketManager { constructor(url, userId) { @@ -10,7 +14,8 @@ class WebSocketManager { this.heartbeatTimer = null; this.isManualClose = false; this.messageQueue = []; - this.maxQueueSize = 1; + this.processingMessages = false; + this.maxQueueSize = 100; this.status = false; } @@ -28,21 +33,28 @@ class WebSocketManager { this.status = true; this.startHeartbeat(); this.processMessageQueue(); + this.dispatch('open'); }; - this.ws.onmessage = (event) => { - if (this.messageQueue.length < this.maxQueueSize) { - this.messageQueue.push(event); - this.processMessageQueue(); - } else { - console.warn('消息队列已满,丢弃消息'); + this.ws.onmessage = async (event) => { + const data = event.data instanceof Blob ? await event.data.arrayBuffer() : event.data; + if (data instanceof ArrayBuffer && this.isMediaPacket(data)) { + this.dispatchMediaPacket(data); + return; } + if (this.messageQueue.length >= this.maxQueueSize) { + console.warn('WebSocket控制消息队列已满,丢弃最旧消息'); + this.messageQueue.shift(); + } + this.messageQueue.push(data); + this.processMessageQueue(); }; this.ws.onclose = () => { console.log('WebSocket 断开'); this.status = false; this.stopHeartbeat(); + this.dispatch('close'); if (!this.isManualClose) { this.reconnect(); } @@ -53,65 +65,80 @@ class WebSocketManager { }; } - async processMessageQueue() { - if (!this.ws || this.messageQueue.length === 0) return; + isMediaPacket(buffer) { + if (buffer.byteLength < MEDIA_HEADER_SIZE) return false; + const bytes = new Uint8Array(buffer, 0, MEDIA_MAGIC.length); + return MEDIA_MAGIC.every((value, index) => bytes[index] === value); + } - const event = this.messageQueue.shift(); + dispatchMediaPacket(buffer) { try { - // 处理消息数据 - console.log('收到消息数据类型:', event.data.constructor.name); - let compressedData; - - // 处理 Blob 或 ArrayBuffer - if (event.data instanceof Blob) { - console.log('处理 Blob 数据,大小:', event.data.size); - compressedData = new Uint8Array(await event.data.arrayBuffer()); - } else if (event.data instanceof ArrayBuffer) { - console.log('处理 ArrayBuffer 数据,大小:', event.data.byteLength); - compressedData = new Uint8Array(event.data); - } else { - throw new Error('不支持的数据类型: ' + event.data.constructor.name); + const view = new DataView(buffer); + const version = view.getUint8(4); + const mediaType = view.getUint8(5); + const timestamp = view.getUint32(8) * 0x100000000 + view.getUint32(12); + const sequence = view.getUint32(16); + const channelLength = view.getUint16(20); + const payloadLength = view.getUint32(22); + const payloadOffset = MEDIA_HEADER_SIZE + channelLength; + if (version !== 1 || payloadOffset + payloadLength !== buffer.byteLength) { + throw new Error('媒体数据包头无效'); } - - // 使用 DecompressionStream 解压 GZIP 数据 - const decompressionStream = new DecompressionStream('gzip'); - const writer = decompressionStream.writable.getWriter(); - writer.write(compressedData); - writer.close(); - - const decompressedStream = decompressionStream.readable; - const reader = decompressedStream.getReader(); - let decompressedData = new Uint8Array(0); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - const newData = new Uint8Array(decompressedData.length + value.length); - newData.set(decompressedData); - newData.set(value, decompressedData.length); - decompressedData = newData; + const channel = new TextDecoder().decode(new Uint8Array(buffer, MEDIA_HEADER_SIZE, channelLength)); + const mimeType = MEDIA_MIME_TYPES[mediaType]; + if (!mimeType) { + throw new Error(`不支持的媒体类型: ${mediaType}`); } + this.dispatch(channel, { + media: true, + mimeType, + sequence, + timestamp, + data: buffer.slice(payloadOffset, payloadOffset + payloadLength) + }); + } catch (error) { + console.error('WebSocket媒体包解析失败:', error); + } + } - // 将解压后的数据转换为字符串 - const textDecoder = new TextDecoder(); - const decompressedString = textDecoder.decode(decompressedData); - console.log('解压后的数据:', JSON.parse(decompressedString)); - - // 解析 JSON - const { type, channel, payload,timestamp } = JSON.parse(decompressedString); - console.log('后端到前端延迟:', ((new Date()).getTime() - timestamp)); - - if (channel && this.events[channel]) { - this.events[channel].forEach(callback => callback(payload)); - } else if (type && this.events[type]) { - this.events[type].forEach(callback => callback(payload)); + async processMessageQueue() { + if (this.processingMessages || !this.ws) return; + this.processingMessages = true; + try { + while (this.messageQueue.length > 0) { + const data = this.messageQueue.shift(); + let message; + if (typeof data === 'string') { + message = JSON.parse(data); + } else if (data instanceof ArrayBuffer) { + const decompressedStream = new Blob([data]) + .stream() + .pipeThrough(new DecompressionStream('gzip')); + const decompressedData = await new Response(decompressedStream).arrayBuffer(); + message = JSON.parse(new TextDecoder().decode(decompressedData)); + } else { + throw new Error(`不支持的WebSocket数据类型: ${typeof data}`); + } + if (message.channel) { + this.dispatch(message.channel, message.payload); + } else if (message.type) { + this.dispatch(message.type, message.payload ?? message.content ?? message); + } } - setTimeout(() => this.processMessageQueue(), 0); } catch (err) { - console.error('WebSocket 消息解析失败:', err); - console.error('错误堆栈:', err.stack); - console.error('原始数据类型:', event.data.constructor.name); - console.error('原始数据大小:', event.data instanceof Blob ? event.data.size : event.data.byteLength || '未知'); + console.error('WebSocket消息解析失败:', err); + } finally { + this.processingMessages = false; + if (this.messageQueue.length > 0) { + queueMicrotask(() => this.processMessageQueue()); + } + } + } + + dispatch(eventName, payload) { + const callbacks = this.events[eventName]; + if (callbacks) { + callbacks.slice().forEach(callback => callback(payload)); } } diff --git a/src/views/device/register/components/Camera/index.vue b/src/views/device/register/components/Camera/index.vue index 466ccb9..a7becbe 100644 --- a/src/views/device/register/components/Camera/index.vue +++ b/src/views/device/register/components/Camera/index.vue @@ -14,30 +14,20 @@ -
-
+
+
- - + + {{ statusText(streamStatus.getDepthImageStream) }}
-
+
- - + + {{ statusText(streamStatus.getRGBImageStream) }}
-
-
- - -
-
- - -
-
@@ -47,18 +37,17 @@ @@ -276,34 +492,19 @@ onUnmounted(() => { flex-direction: column; flex-grow: 1; overflow: hidden; - .single-stream-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - flex-grow: 1; - .stream-wrapper { - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - overflow: hidden; - position: relative; - video { - width: auto; - height: 100%; - object-fit: contain; - transition: opacity 0.1s ease-in-out; /* 平滑过渡 */ - } - } - } .stream-container { display: flex; flex-direction: column; flex-grow: 1; + min-height: 0; + &.dual { + .stream-item { + min-height: 50%; + } + } .stream-item { flex: 1; + min-height: 0; display: flex; flex-direction: column; justify-content: center; @@ -317,14 +518,27 @@ onUnmounted(() => { overflow: hidden; position: relative; video { - width: auto; + width: 100%; height: 100%; object-fit: contain; - transition: opacity 0.1s ease-in-out; /* 平滑过渡 */ + background: #000; + } + .stream-state { + position: absolute; + top: 12px; + right: 12px; + padding: 4px 9px; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 4px; + background: rgba(0, 0, 0, 0.58); + color: rgba(255, 255, 255, 0.9); + font-size: 12px; + line-height: 18px; + pointer-events: none; } } } } } } - \ No newline at end of file + diff --git a/src/views/flow/components/params/BasicNodeParams.vue b/src/views/flow/components/params/BasicNodeParams.vue index ee8eef7..b79b875 100644 --- a/src/views/flow/components/params/BasicNodeParams.vue +++ b/src/views/flow/components/params/BasicNodeParams.vue @@ -219,7 +219,21 @@ const testRule = (property) => { } else { return Promise.reject(new Error('加速度必须大于速度')) } - }, trigger: 'blur' } + }, trigger: 'change' } + ] + + } + + if (property?.name === 'velocity') { + return [ + { required: true, message: '请输入参数值', trigger: 'blur' }, + { validator: () => { + if (formData.nodeParams.find(param => param.name === 'acceleration')?.input > formData.nodeParams.find(param => param.name === 'velocity')?.input) { + return Promise.resolve() + } else { + return Promise.reject(new Error('加速度必须大于速度')) + } + }, trigger: 'change' } ] }