diff --git a/src/assets/images/robot.png b/src/assets/images/robot.png new file mode 100644 index 0000000..3d8bae6 Binary files /dev/null and b/src/assets/images/robot.png differ diff --git a/src/views/device/register/components/Camera/index.vue b/src/views/device/register/components/Camera/index.vue index bdf557d..7c94eac 100644 --- a/src/views/device/register/components/Camera/index.vue +++ b/src/views/device/register/components/Camera/index.vue @@ -3,7 +3,7 @@
- + @@ -17,21 +17,25 @@
- Depth Image + +
- Color Image + +
- Depth Image + +
- Color Image + +
@@ -46,6 +50,7 @@ import { inject, reactive, toRefs, watch, onMounted, onUnmounted, ref, nextTick } from 'vue'; import { getRegister } from "@/api/device/register"; import { useRoute } from "vue-router"; +import { debounce } from 'lodash'; // 引入 lodash 的防抖函数 const socket = inject('ws'); const route = useRoute(); @@ -53,46 +58,49 @@ const data = reactive({ form: { stereoModule: false, rgbCamera: false, - }, terminalId: null, cameraId: null, type: 'camera', - callbacks: {} + callbacks: {}, }); const { form } = toRefs(data); -const depthImage = ref(null); -const colorImage = ref(null); +const depthVideo = ref(null); +const depthVideoBuffer = ref(null); +const colorVideo = ref(null); +const colorVideoBuffer = ref(null); const blobUrls = ref([]); const frameData = reactive({ - getRGBImageStream : { count: 0, lastTime: 0 }, - getDepthImageStream: { count: 0, lastTime: 0 } + getRGBImageStream: { count: 0, lastTime: 0 }, + getDepthImageStream: { count: 0, lastTime: 0 }, }); -function renderImage(imageRef, data, channel, imageType) { - const image = imageRef.value; // 动态获取最新 ref - if (!image) { - console.error(`图像元素未找到 (${imageType}): 通道: ${channel}`); +function renderVideo(mainRef, bufferRef, data, channel, imageType) { + const mainVideo = mainRef.value; + const bufferVideo = bufferRef.value; + if (!mainVideo || !bufferVideo) { + console.error(`视频元素未找到 (${imageType}): 通道: ${channel}`); return; } + const start = performance.now(); try { if (typeof data === 'string') { - if (data.startsWith('blob:')) { - if (image.src && image.src.startsWith('blob:')) { - URL.revokeObjectURL(image.src); - blobUrls.value = blobUrls.value.filter(url => url !== image.src); + const newSrc = data.startsWith('blob:') ? data : `data:video/mp4;base64,${data}`; + + // 先加载到缓冲视频元素 + bufferVideo.src = newSrc; + bufferVideo.onloadeddata = () => { + // 缓冲视频加载完成后,切换到主视频 + if (mainVideo.src && mainVideo.src.startsWith('blob:')) { + URL.revokeObjectURL(mainVideo.src); + blobUrls.value = blobUrls.value.filter(url => url !== mainVideo.src); } - image.src = data; - blobUrls.value.push(data); - } else { - if (image.src && image.src.startsWith('blob:')) { - URL.revokeObjectURL(image.src); - blobUrls.value = blobUrls.value.filter(url => url !== image.src); - } - image.src = `data:image/jpeg;base64,${data}`; - } - console.log(`更新 image.src (${imageType}): ${image.src.substring(0, 50)}, 通道: ${channel}`); + mainVideo.src = bufferVideo.src; + blobUrls.value.push(newSrc); + bufferVideo.src = ''; // 清空缓冲视频源 + }; + frameData[imageType].count++; const currentTime = performance.now(); if (frameData[imageType].lastTime && currentTime - frameData[imageType].lastTime >= 1000) { @@ -103,23 +111,23 @@ function renderImage(imageRef, data, channel, imageType) { } else if (!frameData[imageType].lastTime) { frameData[imageType].lastTime = currentTime; } - console.log(`图像渲染耗时 (${imageType}): ${performance.now() - start}ms, 通道: ${channel}`); + console.log(`视频渲染耗时 (${imageType}): ${performance.now() - start}ms, 通道: ${channel}`); if (performance.memory) { console.log('当前内存使用:', { usedJSHeapSize: (performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB', - totalJSHeapSize: (performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB' + totalJSHeapSize: (performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB', }); } } else { console.error(`未知 payload 类型 (${imageType}):`, typeof data, '通道:', channel); } } catch (err) { - console.error(`图像渲染失败 (${imageType}):`, err, '通道:', channel); - console.error('payload 数据:', data); + console.error(`视频渲染失败 (${imageType}):`, err, '通道:', channel); } } -const sunbcribe = async (sub, terminalId, method) => { +// 防抖订阅函数 +const subscribeDebounced = debounce(async (sub, terminalId, method) => { if (!socket || !terminalId || !data.cameraId) { console.warn(`订阅失败: socket=${!!socket}, terminalId=${terminalId}, cameraId=${data.cameraId}, method=${method}`); return; @@ -129,14 +137,15 @@ const sunbcribe = async (sub, terminalId, method) => { socket.send({ type: 'channel_subscription', action: sub ? 'subscribe' : 'unsubscribe', - channel + channel, }); if (sub) { await nextTick(); - const imageRef = method === 'getRGBImageStream' ? colorImage : depthImage; - if (!imageRef.value) { - console.error(`图像元素未找到: ${method}, 通道: ${channel}`); + const mainRef = method === 'getRGBImageStream' ? colorVideo : depthVideo; + const bufferRef = method === 'getRGBImageStream' ? colorVideoBuffer : depthVideoBuffer; + if (!mainRef.value || !bufferRef.value) { + console.error(`视频元素未找到: ${method}, 通道: ${channel}`); return; } @@ -147,11 +156,12 @@ const sunbcribe = async (sub, terminalId, method) => { } const callback = (data) => { - if(!(data == '500')) { - requestAnimationFrame(() => renderImage(imageRef, data, channel, method)); + if (data !== '500') { + requestAnimationFrame(() => renderVideo(mainRef, bufferRef, data, channel, method)); } else { - sunbcribe(false, terminalId, method) - sunbcribe(true, terminalId, method) + // 错误处理:取消订阅后重新订阅 + subscribeDebounced(false, terminalId, method); + subscribeDebounced(true, terminalId, method); } }; socket.on(channel, callback); @@ -164,36 +174,19 @@ const sunbcribe = async (sub, terminalId, method) => { console.log('清理回调:', channel); } } -}; - -// 监听 ref 变化,重新订阅 -watch(depthImage, async (newVal, oldVal) => { - if (newVal !== oldVal && data.form.stereoModule && data.terminalId && data.cameraId) { - console.log('depthImage ref 变化,重新订阅 stereoModule'); - await sunbcribe(false, data.terminalId, 'getDepthImageStream'); - await sunbcribe(true, data.terminalId, 'getDepthImageStream'); - } -}); - -watch(colorImage, async (newVal, oldVal) => { - if (newVal !== oldVal && data.form.rgbCamera && data.terminalId && data.cameraId) { - console.log('colorImage ref 变化,重新订阅 rgbCamera'); - await sunbcribe(false, data.terminalId, 'getRGBImageStream'); - await sunbcribe(true, data.terminalId, 'getRGBImageStream'); - } -}); - +}, 300); // 300ms 防抖延迟 +// 监听开关变化 watch(() => data.form.stereoModule, async (newVal) => { if (!data.terminalId || !data.cameraId) return; await nextTick(); - await sunbcribe(newVal, data.terminalId, 'getDepthImageStream'); + subscribeDebounced(newVal, data.terminalId, 'getDepthImageStream'); }); watch(() => data.form.rgbCamera, async (newVal) => { if (!data.terminalId || !data.cameraId) return; await nextTick(); - await sunbcribe(newVal, data.terminalId, 'getRGBImageStream'); + subscribeDebounced(newVal, data.terminalId, 'getRGBImageStream'); }); onMounted(() => { @@ -205,9 +198,11 @@ onMounted(() => { onUnmounted(() => { if (socket && data.terminalId && data.cameraId) { - socket.off(`edgeCameraServiceImpl/getRGBImageStream/${data.terminalId}/${data.cameraId}`); - socket.off(`edgeCameraServiceImpl/getDepthImageStream//${data.terminalId}/${data.cameraId}`); - Object.keys(data.callbacks).forEach(channel => { + const channels = [ + `edgeCameraServiceImpl/getRGBImageStream/${data.terminalId}/${data.cameraId}`, + `edgeCameraServiceImpl/getDepthImageStream/${data.terminalId}/${data.cameraId}`, + ]; + channels.forEach(channel => { socket.off(channel, data.callbacks[channel]); console.log('卸载时清理回调:', channel); }); @@ -223,13 +218,10 @@ onUnmounted(() => { display: flex; flex-direction: column; flex: 1; - // height: calc(100vh - 124px); background-color: #000; - .height-full { height: 100%; } - .control-panel { padding: 10px; .controls { @@ -242,20 +234,17 @@ onUnmounted(() => { } } } - .video-panel { display: flex; 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%; @@ -263,27 +252,25 @@ onUnmounted(() => { justify-content: center; align-items: center; overflow: hidden; - - img { + 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; - .stream-item { flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; - .stream-wrapper { width: 100%; height: 100%; @@ -291,11 +278,12 @@ onUnmounted(() => { justify-content: center; align-items: center; overflow: hidden; - - img { + position: relative; + video { width: auto; height: 100%; object-fit: contain; + transition: opacity 0.1s ease-in-out; /* 平滑过渡 */ } } } diff --git a/src/views/device/register/components/DexHand/index.vue b/src/views/device/register/components/DexHand/index.vue index db241fb..4092f67 100644 --- a/src/views/device/register/components/DexHand/index.vue +++ b/src/views/device/register/components/DexHand/index.vue @@ -60,8 +60,8 @@ const handleUpdateBottomSeriesData = ({ maxData, avgData }) => { \ No newline at end of file