diff --git a/src/components/AgrtcRoomRobot.tsx b/src/components/AgrtcRoomRobot.tsx new file mode 100644 index 0000000..92d0c3e --- /dev/null +++ b/src/components/AgrtcRoomRobot.tsx @@ -0,0 +1,217 @@ +import React, { useEffect, useRef, useState } from "react"; +import AgoraRTC, { IAgoraRTCClient, IRemoteVideoTrack } from "agora-rtc-sdk-ng"; +import { Card, Badge, message, Space } from "antd"; + +const APP_ID = "875f6494d87548e783fefbf33f598c97"; +const TOKEN = "007eJxTYIjPuZr09PC0znd6+z0MMyL6ZHb68OetXW+kO1vHU2ZNtYECg4W5aZqZiaVJCpBhYpFqbmGclpqWlGZsnGZqaZFsaT6pZH5WQyAjA7e+LysjAwSC+JwMxmYlqcUlugZGDAwAbuYeJg=="; + +export default function AgrtcRoomRobot({ CHANNEL }) { + const [uid] = useState(() => Number(Math.floor(Math.random() * 100000))); + const [connected, setConnected] = useState(false); + const [joined, setJoined] = useState(false); + + const remoteVideoRef = useRef(null); + const clientRef = useRef(null); + const remoteVideoTrackRef = useRef(null); + + const joiningRef = useRef(false); + const reconnectTimerRef = useRef(null); + + useEffect(() => { + const client = AgoraRTC.createClient({ + mode: "live", + codec: "h264", + }); + clientRef.current = client; + client.setClientRole("audience"); + + let isUnmounted = false; + + // 停止并清空远端视频 + const clearRemoteVideo = () => { + if (remoteVideoTrackRef.current) { + try { + remoteVideoTrackRef.current.stop(); + } catch (e) { /* */ } + remoteVideoTrackRef.current = null; + } + if (remoteVideoRef.current) { + remoteVideoRef.current.innerHTML = ""; + } + }; + + // 重连逻辑 + const doReconnect = async () => { + if (isUnmounted || joiningRef.current) return; + console.log("执行RTC重连"); + clearRemoteVideo(); + setJoined(false); + + try { + joiningRef.current = true; + await client.join(APP_ID, CHANNEL, TOKEN, uid); + if (isUnmounted) return; + setJoined(true); + message.info("RTC重连成功"); + + // 重连成功后重新订阅已经在线的上位机 + for (const remoteUser of client.remoteUsers) { + if (isUnmounted) break; + if (remoteUser.hasVideo) { + await client.subscribe(remoteUser, "video"); + remoteVideoTrackRef.current = remoteUser.videoTrack; + remoteUser.videoTrack?.play(remoteVideoRef.current!); + } + } + } catch (err: any) { + if (err?.code !== "OPERATION_ABORTED") { + console.error("重连失败", err); + } + } finally { + joiningRef.current = false; + } + }; + + // 连接状态变化 + const onConnStateChange = (curState: string, prevState: string) => { + if (isUnmounted) return; + console.log("连接状态变更", prevState, "→", curState); + setConnected(curState === "CONNECTED"); + + // 信令断开,延迟重连 + if (curState === "DISCONNECTED") { + clearRemoteVideo(); + if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current); + reconnectTimerRef.current = setTimeout(() => { + doReconnect(); + }, 2000); + } + }; + + // 远端用户发布流 + const onUserPublished = async (user: any, mediaType: "video" | "audio") => { + if (isUnmounted) return; + console.log("user-published", user.uid, mediaType); + await client.subscribe(user, mediaType); + + if (mediaType === "video" && remoteVideoRef.current) { + clearRemoteVideo(); + remoteVideoTrackRef.current = user.videoTrack; + user.videoTrack?.play(remoteVideoRef.current); + } + }; + + // 远端停止发布流(上位机停止推流) + const onUserUnpublished = (user: any, mediaType: "video" | "audio") => { + if (isUnmounted) return; + console.log("user-unpublished", user.uid, mediaType); + if (mediaType === "video") { + clearRemoteVideo(); + } + }; + + // 远端用户离开频道 + const onUserLeft = (user: any) => { + if (isUnmounted) return; + console.log("user-left", user.uid); + clearRemoteVideo(); + }; + + // 远端用户进入 + const onUserJoined = (user: any) => { + if (!isUnmounted) console.log("远端用户进入频道:", user.uid); + }; + + // Token即将过期提醒(测试Token有效期有限) + const onTokenWillExpire = () => { + console.warn("token即将过期,请更新token"); + message.warning("Token即将过期,视频可能很快断开"); + // 正式环境:请求后端拿新token client.renewToken(newToken) + }; + + // 注册事件,保存回调引用用于off解绑 + client.on("connection-state-change", onConnStateChange); + client.on("user-published", onUserPublished); + client.on("user-unpublished", onUserUnpublished); + client.on("user-left", onUserLeft); + client.on("user-joined", onUserJoined); + client.on("token-privilege-will-expire", onTokenWillExpire); + + const init = async () => { + if (joiningRef.current) return; + joiningRef.current = true; + try { + await client.join(APP_ID, CHANNEL, TOKEN, uid); + if (isUnmounted) { + console.log("组件已卸载,丢弃join结果"); + return; + } + console.log("加入频道成功"); + setJoined(true); + message.success("RTC已加入频道"); + + // 上位机已经提前推流,手动订阅 + for (const remoteUser of client.remoteUsers) { + if (isUnmounted) break; + console.log("发现已有远端用户:", remoteUser.uid); + if (remoteUser.hasVideo) { + await client.subscribe(remoteUser, "video"); + remoteVideoTrackRef.current = remoteUser.videoTrack; + remoteUser.videoTrack?.play(remoteVideoRef.current!); + } + } + } catch (err: any) { + if (err?.code === "OPERATION_ABORTED") { + console.log("join操作被取消(StrictMode/组件卸载),忽略"); + return; + } + console.error("加入频道失败:", err); + if (!isUnmounted) { + message.error("加入频道失败,请检查appid/token/channel"); + } + } finally { + joiningRef.current = false; + } + }; + + init(); + + // 组件卸载清理 + return () => { + isUnmounted = true; + if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current); + clearRemoteVideo(); + + const c = clientRef.current; + if (!c) return; + + c.off("connection-state-change", onConnStateChange); + c.off("user-published", onUserPublished); + c.off("user-unpublished", onUserUnpublished); + c.off("user-left", onUserLeft); + c.off("user-joined", onUserJoined); + c.off("token-privilege-will-expire", onTokenWillExpire); + + c.leave(); + clientRef.current = null; + }; + }, [uid]); + + return ( + + +
当前 UID:{uid}
+ + + +
+

远端视频(上位机画面)

+
+
+ + + ); +} diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 50043bb..c728e5c 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -564,8 +564,8 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine, tooltip.innerHTML = `
🤖 ${robot.deviceAlias || robot.serialNumber || robot.name}
${deviceValuesHTML ? `
${t('devices.deviceData')}:${deviceValuesHTML}
` : ''} -
${t('systemSetting.longitude')}:${Number(robot.lastRunningStatus?.longitude).toFixed(6)}
-
${t('systemSetting.latitude')}:${Number(robot.lastRunningStatus?.latitude).toFixed(6)}
+
${t('systemSetting.longitude')}:${Number(robot?.locationMessage?.longitude || robot?.lastRunningStatus?.longitude).toFixed(6)}
+
${t('systemSetting.latitude')}:${Number(robot?.locationMessage?.latitude || robot?.lastRunningStatus?.latitude).toFixed(6)}
`; } @@ -1403,7 +1403,7 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine, devices.forEach((device, index) => { - const raw = device?.lastRunningStatus || {}; + const raw = device?.locationMessage || device?.lastRunningStatus || {}; const parsed = parseLngLat(raw.longitude, raw.latitude); if (!parsed) return; diff --git a/src/components/devices/DeviceControl.tsx b/src/components/devices/DeviceControl.tsx index 82d7007..65be71c 100644 --- a/src/components/devices/DeviceControl.tsx +++ b/src/components/devices/DeviceControl.tsx @@ -50,8 +50,15 @@ interface Robot { lastRunningStatus?: { latitude: string | number; longitude: string | number; - battery: number; + battery: string | number; }; + locationMessage?: { + latitude: string | number; + longitude: string | number; + battery: string | number; + + }; + drone_callsign?: string; serialNumber?: string; } @@ -1040,8 +1047,8 @@ const RobotControlCenter = ({ setView }) => { }; const mapFocus = selectedDevice ? { - lat: selectedDevice.lastRunningStatus.latitude, - lon: selectedDevice.lastRunningStatus.longitude, + lat: selectedDevice?.locationMessage?.latitude || selectedDevice?.lastRunningStatus?.latitude, + lon: selectedDevice?.locationMessage?.longitude || selectedDevice?.lastRunningStatus?.longitude, height: 500 } : { lat: '31.030129089704428', lon: '120.76671769055027', height: 800 }; //手柄数据 diff --git a/src/components/devices/RobotTaskPage.tsx b/src/components/devices/RobotTaskPage.tsx index 68cbebe..c3df361 100644 --- a/src/components/devices/RobotTaskPage.tsx +++ b/src/components/devices/RobotTaskPage.tsx @@ -306,7 +306,7 @@ export default function RobotTaskPage() { const runningTask = taskPoolList.find(item => ['EXECUTING', 'PAUSE'].includes(item.taskStaus)); if (!runningTask) return; - fetchRouteDetail(runningTask?.id); + //fetchRouteDetail(runningTask?.id); // 找到对应的设备 const targetDevice = devices.find(d => d.serialNumber === runningTask.deviceId); @@ -510,6 +510,8 @@ export default function RobotTaskPage() { if (!runningPoolTask?.id) return; loadTaskPath(runningPoolTask.id); + fetchRouteDetail(runningPoolTask?.id); + // 只有当任务是执行状态,并且没有连接的设备时,才自动连接 if ((runningPoolTask.taskStaus == 'EXECUTING' || runningPoolTask.taskStaus == 'PAUSE') && !executingRobot) {