声网组件接入 优化刷新之后路线的显示 兼容上位机最后一次经纬度 的 数据

This commit is contained in:
mmc
2026-09-08 15:30:29 +08:00
parent 13bb3ca633
commit 6e4083c88c
4 changed files with 233 additions and 7 deletions

View File

@@ -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<HTMLDivElement>(null);
const clientRef = useRef<IAgoraRTCClient | null>(null);
const remoteVideoTrackRef = useRef<IRemoteVideoTrack | null>(null);
const joiningRef = useRef(false);
const reconnectTimerRef = useRef<NodeJS.Timeout | null>(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 (
<Card title="Agora RTC 上位机视频观看" style={{ width: 1000, margin: "20px auto" }}>
<Space direction="vertical" style={{ width: "100%" }}>
<div>当前 UID:{uid}</div>
<Badge color={connected ? "green" : "red"} text={connected ? "RTC 已连接" : "RTC 未连接"} />
<Badge color={joined ? "green" : "orange"} text={joined ? "已加入频道" : "未加入频道"} />
<div style={{ marginTop: 20 }}>
<h3>远端视频(上位机画面)</h3>
<div
ref={remoteVideoRef}
style={{ width: 800, height: 600, background: "#000", borderRadius: 8, overflow: "hidden" }}
/>
</div>
</Space>
</Card>
);
}

View File

@@ -564,8 +564,8 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
tooltip.innerHTML = `
<div style="font-weight:600;margin-bottom:6px;">🤖 ${robot.deviceAlias || robot.serialNumber || robot.name}</div>
${deviceValuesHTML ? `<div style="margin-top:8px;"><strong>${t('devices.deviceData')}:</strong>${deviceValuesHTML}</div>` : ''}
<div>${t('systemSetting.longitude')}:${Number(robot.lastRunningStatus?.longitude).toFixed(6)}</div>
<div>${t('systemSetting.latitude')}:${Number(robot.lastRunningStatus?.latitude).toFixed(6)}</div>
<div>${t('systemSetting.longitude')}:${Number(robot?.locationMessage?.longitude || robot?.lastRunningStatus?.longitude).toFixed(6)}</div>
<div>${t('systemSetting.latitude')}:${Number(robot?.locationMessage?.latitude || robot?.lastRunningStatus?.latitude).toFixed(6)}</div>
`;
}
@@ -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;

View File

@@ -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 };
//手柄数据

View File

@@ -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) {