无人机组件 终于可以显示两个视频了!!!!
This commit is contained in:
@@ -224,3 +224,12 @@ export function getAllDevice(data) {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export function getPlaneDeviceListAll() {
|
||||
return request({
|
||||
url: '/iot/UAV/getProjectDeviceList',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -707,7 +707,18 @@ export default function CesiumMap({ realtimePosition, pathPoints = [], devices =
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 禁用聚焦飞行
|
||||
if (!viewerRef.current || !focusLocation) return;
|
||||
const viewer = viewerRef.current;
|
||||
|
||||
viewer.camera.flyTo({
|
||||
destination: window.Cesium.Cartesian3.fromDegrees(Number(focusLocation.lon), Number(focusLocation.lat), focusLocation.height),
|
||||
orientation: {
|
||||
heading: window.Cesium.Math.toRadians(0.0),
|
||||
pitch: window.Cesium.Math.toRadians(-90.0),
|
||||
roll: 0.0,
|
||||
},
|
||||
duration: 1.5
|
||||
});
|
||||
}, [focusLocation]);
|
||||
|
||||
function flyToRobots(viewer, robots) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { joinRoom, leaveRoom } from "./droneplayer";
|
||||
import { DroneRTC } from "./droneplayer";
|
||||
import { postLiveHeartbeat } from "@/api/api";
|
||||
|
||||
export default function DroneLivePlayer({
|
||||
@@ -7,179 +7,70 @@ export default function DroneLivePlayer({
|
||||
videoMode = "NORMAL",
|
||||
}) {
|
||||
const containerRef = useRef(null);
|
||||
const rtcRef = useRef(null);
|
||||
|
||||
// 心跳 timer
|
||||
const timerRef = useRef(null);
|
||||
|
||||
// 当前 effect 是否有效(防止异步穿透)
|
||||
const aliveRef = useRef(false);
|
||||
|
||||
// 防止 join / leave 并发
|
||||
const busyRef = useRef(false);
|
||||
// 心跳
|
||||
const heartbeatTimerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
|
||||
// 当前 effect 生效
|
||||
aliveRef.current = true;
|
||||
|
||||
if (!container) return;
|
||||
|
||||
// 强制清空旧 DOM
|
||||
container.innerHTML = "";
|
||||
|
||||
// 没有流地址直接退出
|
||||
if (!streamData?.url) {
|
||||
return;
|
||||
}
|
||||
if (!streamData?.url) return;
|
||||
|
||||
// ---------------- 解析 RTC 参数 ----------------
|
||||
const params = new URLSearchParams(streamData.url);
|
||||
|
||||
const params = new URLSearchParams(streamData?.url);
|
||||
|
||||
const appId = (
|
||||
const appId =
|
||||
params.get("app_id") ||
|
||||
params.get("appid") ||
|
||||
""
|
||||
).trim();
|
||||
"";
|
||||
|
||||
const roomId = (
|
||||
const roomId =
|
||||
params.get("room_id") ||
|
||||
params.get("roomid") ||
|
||||
""
|
||||
).trim();
|
||||
"";
|
||||
|
||||
const token = decodeURIComponent(
|
||||
(params.get("token") || "").trim()
|
||||
params.get("token") || ""
|
||||
);
|
||||
|
||||
const userId = (
|
||||
const userId =
|
||||
params.get("user_id") ||
|
||||
params.get("uid") ||
|
||||
""
|
||||
).trim();
|
||||
"";
|
||||
|
||||
if (!appId || !roomId || !token || !userId) {
|
||||
console.error("RTC 参数缺失");
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------------- 心跳 ----------------
|
||||
|
||||
const liveheartbeat = async () => {
|
||||
try {
|
||||
await postLiveHeartbeat({
|
||||
sn: streamData?.sn,
|
||||
cameraIndex: streamData?.camera_index,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("心跳失败", err);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------- 初始化 RTC ----------------
|
||||
// ✅ 关键:每个视频创建自己的 DroneRTC 实例
|
||||
const rtc = new DroneRTC(appId);
|
||||
rtcRef.current = rtc;
|
||||
|
||||
const initRTC = async () => {
|
||||
// 防止重复进入
|
||||
if (busyRef.current) return;
|
||||
|
||||
busyRef.current = true;
|
||||
|
||||
try {
|
||||
// =========================
|
||||
// 1. 先彻底关闭旧 RTC
|
||||
// =========================
|
||||
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
|
||||
// 必须 await
|
||||
await leaveRoom();
|
||||
|
||||
// 给 SDK 一点释放时间
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, 300)
|
||||
);
|
||||
|
||||
// 组件已经卸载
|
||||
if (!aliveRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// 2. 加入新房间
|
||||
// =========================
|
||||
|
||||
await joinRoom(
|
||||
container,
|
||||
appId,
|
||||
roomId,
|
||||
token,
|
||||
userId,
|
||||
videoMode
|
||||
);
|
||||
|
||||
// 再次检查
|
||||
if (!aliveRef.current) {
|
||||
await leaveRoom();
|
||||
return;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// 3. 开启心跳
|
||||
// =========================
|
||||
|
||||
/*
|
||||
timerRef.current = setInterval(() => {
|
||||
liveheartbeat();
|
||||
}, 3000);
|
||||
|
||||
liveheartbeat();
|
||||
*/
|
||||
} catch (err) {
|
||||
console.error("RTC 初始化失败:", err);
|
||||
|
||||
// 出错强制释放
|
||||
try {
|
||||
await leaveRoom();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} finally {
|
||||
busyRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
initRTC();
|
||||
|
||||
// =========================
|
||||
// cleanup
|
||||
// =========================
|
||||
rtc.joinRoom(container, roomId, token, userId);
|
||||
|
||||
// ---------------- cleanup ----------------
|
||||
return () => {
|
||||
aliveRef.current = false;
|
||||
|
||||
// 清理心跳
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
timerRef.current = null;
|
||||
if (heartbeatTimerRef.current) {
|
||||
clearInterval(heartbeatTimerRef.current);
|
||||
}
|
||||
|
||||
// 异步释放 RTC
|
||||
(async () => {
|
||||
try {
|
||||
await leaveRoom();
|
||||
|
||||
// 清空 DOM
|
||||
if (containerRef.current) {
|
||||
containerRef.current.innerHTML = "";
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("leaveRoom error:", err);
|
||||
}
|
||||
})();
|
||||
const rtc = rtcRef.current;
|
||||
if (rtc) {
|
||||
rtcRef.current = null;
|
||||
setTimeout(() => rtc.destroy(), 0);
|
||||
}
|
||||
if (container) {
|
||||
container.innerHTML = "";
|
||||
}
|
||||
};
|
||||
}, [
|
||||
streamData?.url,
|
||||
@@ -191,7 +82,6 @@ export default function DroneLivePlayer({
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="rounded"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import {
|
||||
Layout, Card, Row, Col, Statistic, Tag, Button, Table,
|
||||
Typography, Space, Progress, Badge, Timeline, message,
|
||||
@@ -59,7 +59,7 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll,
|
||||
const isUAV = !!selectedDevice?.gateway_sn;
|
||||
|
||||
const { video: robotVideo, error, ready } = useWebRTCPlayer(
|
||||
selectedDevice?.serialNumber || '',
|
||||
!isUAV ? selectedDevice?.serialNumber || '' : '',
|
||||
`webrtc://1.95.137.212/live/livestream/${selectedDevice?.serialNumber}`,
|
||||
);
|
||||
|
||||
@@ -111,6 +111,15 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll,
|
||||
if (onDeviceSelect) onDeviceSelect(id);
|
||||
};
|
||||
|
||||
// 用于视频、状态展示的实时设备数据(只合并一次)
|
||||
const realtimeDevice = useMemo(() => {
|
||||
if (!selectedDevice) return null;
|
||||
return {
|
||||
...selectedDevice,
|
||||
...uavState, // 无人机实时状态覆盖
|
||||
};
|
||||
}, [selectedDevice, uavState]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUavState = async () => {
|
||||
@@ -131,7 +140,7 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll,
|
||||
}
|
||||
};
|
||||
|
||||
fetchUavState();
|
||||
fetchUavState();
|
||||
|
||||
|
||||
}, [isUAV, selectedDevice]);
|
||||
@@ -184,7 +193,7 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll,
|
||||
{ title: '类型', dataIndex: 'type', width: 90 },
|
||||
{ title: '当前状态', dataIndex: 'status', width: 70, render: s => <Tag color={s === '作业中' || s === '在线' ? 'success' : 'default'} style={{ margin: 0 }}>{s}</Tag> },
|
||||
{ title: '模式', dataIndex: 'mode', width: 70 },
|
||||
{ title: '电量', dataIndex: 'battery', width: 70, render: v => <Progress percent={v} size="small" status={v < 40 ? 'exception' : 'active'} /> },
|
||||
{ title: '电量', dataIndex: 'capacity_percent', width: 70, render: v => <Progress percent={v} size="small" status={v < 40 ? 'exception' : 'active'} /> },
|
||||
{ title: '位置', dataIndex: 'position', width: 90 },
|
||||
{ title: '当前任务', dataIndex: 'task', width: 100 },
|
||||
{ title: '通讯状态', dataIndex: 'comm', width: 70, render: v => <Badge status={v.includes('-') ? 'warning' : 'success'} text={v} /> },
|
||||
@@ -344,10 +353,9 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll,
|
||||
<div style={{ width: '100%', height: 160, borderRadius: 8, position: 'relative', overflow: 'hidden', background: '#000' }}>
|
||||
{isUAV ? (
|
||||
<DroneVideoPlayer
|
||||
device={selectedDevice}
|
||||
device={realtimeDevice}
|
||||
height={160}
|
||||
wind_speed={uavState?.wind_speed}
|
||||
h={uavState?.height}
|
||||
|
||||
showSourceSelect={false}
|
||||
videoSource={videoSource}
|
||||
onSourceChange={setVideoSource}
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import {
|
||||
getsinglePlan,
|
||||
getsinglePlanTrack,
|
||||
getFlightTaskDetail,
|
||||
getUAVState,
|
||||
@@ -85,6 +86,7 @@ export default function DeviceStatusPage({
|
||||
|
||||
const [mowerView, setMowerView] = useState('front');
|
||||
const [uavState, setUavState] = useState<any>(null);
|
||||
const [points, setPoints] = useState([]);
|
||||
const timerRef = useRef<any>(null);
|
||||
|
||||
const device = useMemo(() => {
|
||||
@@ -116,7 +118,7 @@ export default function DeviceStatusPage({
|
||||
}, [device, wsDeviceInfo, isUAV]);
|
||||
|
||||
const { video: robotVideo, error, ready } = useWebRTCPlayer(
|
||||
serialNumber || '',
|
||||
!isUAV ? serialNumber : '',
|
||||
`webrtc://1.95.137.212/live/livestream/${serialNumber}`,
|
||||
);
|
||||
const getVal = (val) => val === "no_rain" ? "无" : (val || '-');
|
||||
@@ -147,10 +149,10 @@ export default function DeviceStatusPage({
|
||||
}, [userInfo, messageApi]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!device || !controllerRef.current) return;
|
||||
if (!device || !controllerRef.current || !device?.serialNumber) return;
|
||||
|
||||
controllerRef.current.connectDevice(device);
|
||||
}, [device]);
|
||||
}, [device?.serialNumber]);
|
||||
|
||||
// 定时获取无人机状态
|
||||
useEffect(() => {
|
||||
@@ -172,35 +174,91 @@ export default function DeviceStatusPage({
|
||||
}
|
||||
};
|
||||
|
||||
fetchUavState();
|
||||
fetchUavState();
|
||||
}, [isUAV, device]);
|
||||
|
||||
// 获取执行中任务的轨迹
|
||||
useEffect(() => {
|
||||
const fetchExecutingTaskTrack = async () => {
|
||||
if (isUAV && device?.gateway_sn) {
|
||||
try {
|
||||
const todayStart = dayjs().startOf('day').unix();
|
||||
const todayEnd = dayjs().endOf('day').unix();
|
||||
const sns = [device.gateway_sn];
|
||||
|
||||
const res = await getsinglePlan({ sns, beginAt: todayStart, endAt: todayEnd });
|
||||
if (res?.code === 200 && res.data) {
|
||||
let combinedList = [];
|
||||
Object.values(res.data).forEach((item: any) => {
|
||||
if (item.list) combinedList = combinedList.concat(item.list);
|
||||
});
|
||||
|
||||
// 筛选出执行中的任务
|
||||
const executingTask = combinedList.find(t =>
|
||||
['executing', 'running', 'restored'].includes(t.status)
|
||||
);
|
||||
|
||||
if (executingTask) {
|
||||
const trackRes = await getsinglePlanTrack(executingTask);
|
||||
if (trackRes.code === 0 && trackRes.data.track?.points?.length) {
|
||||
const trackPoints = trackRes.data.track.points.map(item => ({
|
||||
lon: item.longitude || item.lon,
|
||||
lat: item.latitude || item.lat
|
||||
}));
|
||||
setPoints(trackPoints);
|
||||
} else {
|
||||
setPoints([]);
|
||||
}
|
||||
} else {
|
||||
setPoints([]);
|
||||
}
|
||||
} else {
|
||||
setPoints([]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取执行中任务轨迹失败', err);
|
||||
setPoints([]);
|
||||
}
|
||||
} else {
|
||||
setPoints([]);
|
||||
}
|
||||
};
|
||||
|
||||
fetchExecutingTaskTrack();
|
||||
}, [isUAV, device]);
|
||||
|
||||
const mapDevices = useMemo(() => {
|
||||
if (!device) return [];
|
||||
if (!device) return null;
|
||||
|
||||
// 优先使用无人机实时数据
|
||||
const lng = uavState?.longitude ?? runningStatus.longitude ?? device.longitude;
|
||||
const lat = uavState?.latitude ?? runningStatus.latitude ?? device.latitude;
|
||||
|
||||
return {
|
||||
...device,
|
||||
lastRunningStatus: {
|
||||
...device.lastRunningStatus,
|
||||
longitude:
|
||||
runningStatus.longitude || device.longitude,
|
||||
latitude:
|
||||
runningStatus.latitude || device.latitude,
|
||||
longitude: lng,
|
||||
latitude: lat,
|
||||
},
|
||||
name:
|
||||
device.deviceAlias ||
|
||||
device.callsign ||
|
||||
device.deviceName ||
|
||||
'设备',
|
||||
// 让地图取最新经纬度
|
||||
longitude: lng,
|
||||
latitude: lat,
|
||||
name: device.deviceAlias || device.callsign || device.deviceName || '设备',
|
||||
type: isUAV
|
||||
? 'drone'
|
||||
: device.productName?.includes('割草机')
|
||||
? 'mower'
|
||||
: 'robot',
|
||||
};
|
||||
}, [device, isUAV, runningStatus]);
|
||||
}, [device, isUAV, runningStatus, uavState]);
|
||||
const realtimeDevice = useMemo(() => {
|
||||
if (!device) return null;
|
||||
return {
|
||||
...device,
|
||||
...uavState, // 无人机实时状态覆盖
|
||||
};
|
||||
}, [device, uavState]);
|
||||
|
||||
const handleGamepadUpdate = useCallback((state) => {
|
||||
controllerRef.current?.sendGamepad(state);
|
||||
@@ -330,8 +388,10 @@ export default function DeviceStatusPage({
|
||||
>
|
||||
{isUAV ? (
|
||||
<DroneVideoPlayer
|
||||
device={device}
|
||||
device={realtimeDevice}
|
||||
|
||||
height={450}
|
||||
|
||||
dualVideo={true}
|
||||
showSourceSelect={false}
|
||||
defaultSource="drone"
|
||||
@@ -432,7 +492,7 @@ export default function DeviceStatusPage({
|
||||
height: 400,
|
||||
}}
|
||||
>
|
||||
<CesiumMap devices={[mapDevices]} />
|
||||
<CesiumMap devices={[mapDevices]} points={points} />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
|
||||
@@ -19,8 +19,7 @@ interface DroneVideoPlayerProps {
|
||||
|
||||
const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
||||
device,
|
||||
wind_speed,
|
||||
h,
|
||||
|
||||
height = 160,
|
||||
showSourceSelect = true,
|
||||
dualVideo = false,
|
||||
@@ -118,26 +117,57 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
||||
const switchDroneCamera = (idx: number) => {
|
||||
setSelectedDroneCameraIdx(idx);
|
||||
};
|
||||
// 用来记住上一次的值,判断哪一路发生了变化
|
||||
const prevRef = useRef({
|
||||
selectedCamera: '',
|
||||
selectedLens: '',
|
||||
});
|
||||
|
||||
// 自动加载
|
||||
useEffect(() => {
|
||||
if (device) {
|
||||
if (dualVideo) {
|
||||
setDroneStreamData(null);
|
||||
if (!device?.device_sn && !device?.gateway_sn) return;
|
||||
|
||||
const prev = prevRef.current;
|
||||
const currentCamera = selectedCamera;
|
||||
const currentLens = selectedLens;
|
||||
|
||||
// 双画面:只刷新【真正变化】的那一路,另一路不动
|
||||
if (dualVideo) {
|
||||
// 只有【机场摄像头】变化了,才刷新机场
|
||||
if (currentCamera !== prev.selectedCamera) {
|
||||
setStationStreamData(null);
|
||||
loadStationCamera(selectedCamera);
|
||||
loadDroneLens(selectedLens);
|
||||
} else {
|
||||
if (videoSource === 'station') {
|
||||
setStationStreamData(null);
|
||||
loadStationCamera(selectedCamera);
|
||||
} else {
|
||||
setDroneStreamData(null);
|
||||
loadDroneLens(selectedLens);
|
||||
}
|
||||
loadStationCamera(currentCamera);
|
||||
}
|
||||
|
||||
// 只有【无人机镜头】变化了,才刷新无人机
|
||||
if (currentLens !== prev.selectedLens) {
|
||||
setDroneStreamData(null);
|
||||
loadDroneLens(currentLens);
|
||||
}
|
||||
|
||||
// 保存最新值,供下一次对比
|
||||
prevRef.current = {
|
||||
selectedCamera: currentCamera,
|
||||
selectedLens: currentLens,
|
||||
};
|
||||
return;
|
||||
}
|
||||
}, [device, videoSource, selectedDroneCameraIdx, dualVideo]);
|
||||
|
||||
// 单画面:保持原有逻辑
|
||||
if (videoSource === 'station') {
|
||||
setStationStreamData(null);
|
||||
loadStationCamera(currentCamera);
|
||||
} else {
|
||||
setDroneStreamData(null);
|
||||
loadDroneLens(currentLens);
|
||||
}
|
||||
}, [
|
||||
device,
|
||||
videoSource,
|
||||
selectedDroneCameraIdx,
|
||||
dualVideo,
|
||||
selectedCamera,
|
||||
selectedLens,
|
||||
]);
|
||||
|
||||
// 生成摄像头下拉选项
|
||||
const droneCameraOptions = device?.drone_camera_list?.map((item: any, idx: number) => ({
|
||||
@@ -157,7 +187,7 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
||||
background: '#000',
|
||||
border: dualVideo ? '1px solid #243157' : 'none',
|
||||
minWidth: dualVideo ? '300px' : 'auto' // 确保在双视频模式下有最小宽度
|
||||
}}>
|
||||
}} key={type}>
|
||||
{/* 视频控制按钮 */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
@@ -214,7 +244,8 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
||||
|
||||
{/* 视频内容 */}
|
||||
{streamData ? (
|
||||
<DroneLivePlayer streamData={streamData} />
|
||||
<DroneLivePlayer streamData={streamData}
|
||||
/>
|
||||
) : (
|
||||
<div style={{
|
||||
color: '#ccc',
|
||||
@@ -263,8 +294,8 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
||||
padding: '4px 8px',
|
||||
fontSize: 10
|
||||
}}>
|
||||
<span>高度 {h?.toFixed(2) || device?.height?.toFixed(2) || '-'}m</span>
|
||||
<span>风速 {wind_speed?.toFixed(2) || device?.wind_speed?.toFixed(2) || '0'}m/s</span>
|
||||
<span>高度 {Number(device?.height)?.toFixed(2) || '-'}m</span>
|
||||
<span>风速 {Number(device?.wind_speed?.toFixed(2) || '0')}m/s</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import dayjs from 'dayjs';
|
||||
import utils from '@/lib/utils';
|
||||
import CesiumMap from '../Map';
|
||||
import { getUAVState } from '@/api/device';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
const { RangePicker } = DatePicker;
|
||||
@@ -47,13 +48,32 @@ export default function WayLinePage({ planedevices, onRefresh }) {
|
||||
|
||||
// ✅ 航线列表 独立无人机筛选(新增)
|
||||
const [waylineDrone, setWaylineDrone] = useState(null);
|
||||
const [focusLocation, setFocusLocation] = useState(null);
|
||||
|
||||
const [points, setPoints] = useState([]);
|
||||
|
||||
const fetchAndFocusDrone = async (drone) => {
|
||||
if (!drone?.gateway_sn || !drone?.device_sn) return;
|
||||
try {
|
||||
const res = await getUAVState({
|
||||
sn: drone.gateway_sn,
|
||||
droneSn: drone.device_sn
|
||||
});
|
||||
if (res.code === 200 && res.data) {
|
||||
setFocusLocation({
|
||||
lon: res.data.longitude,
|
||||
lat: res.data.latitude
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取无人机位置失败', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 航线列表相关状态
|
||||
const [selectedWayline, setSelectedWayline] = useState(null);
|
||||
const [rthModalVisible, setRthModalVisible] = useState(false);
|
||||
const [tempRthAltitude, setTempRthAltitude] = useState(100);
|
||||
const [tempRthAltitude, setTempRthAltitude] = useState(20);
|
||||
const [waylineSearch, setWaylineSearch] = useState('');
|
||||
|
||||
// 任务控制相关状态
|
||||
@@ -75,7 +95,7 @@ export default function WayLinePage({ planedevices, onRefresh }) {
|
||||
name: '',
|
||||
sn: '',
|
||||
landing_dock_sn: '',
|
||||
rth_altitude: 100,
|
||||
rth_altitude: 10,
|
||||
rth_mode: 'optimal',
|
||||
wayline_precision_type: 'gps',
|
||||
resumable_status: 'auto',
|
||||
@@ -144,7 +164,7 @@ export default function WayLinePage({ planedevices, onRefresh }) {
|
||||
name: '',
|
||||
sn: waylineDrone?.gateway_sn || '',
|
||||
landing_dock_sn: '',
|
||||
rth_altitude: 100,
|
||||
rth_altitude: 10,
|
||||
rth_mode: 'optimal',
|
||||
wayline_precision_type: 'gps',
|
||||
resumable_status: 'auto',
|
||||
@@ -413,9 +433,12 @@ export default function WayLinePage({ planedevices, onRefresh }) {
|
||||
return;
|
||||
}
|
||||
setSelectedWayline(wayline);
|
||||
setTempRthAltitude(100);
|
||||
setTempRthAltitude(20);
|
||||
setRthModalVisible(true);
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchAndFocusDrone(waylineDrone);
|
||||
}, [waylineDrone])
|
||||
|
||||
// 确认执行航线任务
|
||||
const confirmExecuteWayline = async () => {
|
||||
@@ -442,7 +465,11 @@ export default function WayLinePage({ planedevices, onRefresh }) {
|
||||
utils.message.success('航线执行任务创建成功');
|
||||
setRthModalVisible(false);
|
||||
getListTask();
|
||||
if (onRefresh) onRefresh();
|
||||
if (onRefresh) {
|
||||
setTimeout(() => {
|
||||
onRefresh();
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
//await getTrackFn({
|
||||
// uuid: res.data.task_uuid
|
||||
@@ -615,7 +642,7 @@ export default function WayLinePage({ planedevices, onRefresh }) {
|
||||
style={{ ...cardStyle, marginBottom: 10 }}
|
||||
>
|
||||
<div style={{ width: '100%', height: 420, background: '#e8f3ff url(https://picsum.photos/seed/solar_map_v2/1600/1000) center/cover', position: 'relative' }}>
|
||||
<CesiumMap points={points} from="device-control" />
|
||||
<CesiumMap devices={planedevices} points={points} focusLocation={focusLocation} from="device-control" />
|
||||
<div style={{ position: 'absolute', top: 16, left: 16, background: 'rgba(255,255,255,0.92)', backdropFilter: 'blur(8px)', padding: '14px 16px', borderRadius: 10, width: 150, boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}>
|
||||
<div style={{ fontSize: fontSmall, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><span style={{ width: 14, height: 2, background: '#165DFF' }}></span> 无人机航线</div>
|
||||
@@ -658,6 +685,7 @@ export default function WayLinePage({ planedevices, onRefresh }) {
|
||||
onChange={(val) => {
|
||||
const find = planedevices.find(i => i.device_sn === val);
|
||||
setListDrone(find || null);
|
||||
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
@@ -677,7 +705,7 @@ export default function WayLinePage({ planedevices, onRefresh }) {
|
||||
}
|
||||
bordered={false}
|
||||
bodyStyle={{ padding: 0, display: 'flex', flexDirection: 'column', height: "calc(100% - 40px)", overflow: "auto" }}
|
||||
style={{ ...cardStyle, height: "240px", flex: 1, marginBottom: 0 }}
|
||||
style={{ ...cardStyle, maxHeight: "250px", flex: 1, marginBottom: 0 }}
|
||||
>
|
||||
<Table
|
||||
size="small"
|
||||
@@ -859,7 +887,7 @@ export default function WayLinePage({ planedevices, onRefresh }) {
|
||||
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>天气窗口与执行约束</span>}
|
||||
bordered={false}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
style={{ ...cardStyle, flex: 1 }}
|
||||
style={{ ...cardStyle, flex: 1, }}
|
||||
>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '8px', marginBottom: 16 }}>
|
||||
<div style={{ background: '#F7F8FA', borderRadius: 10, padding: '12px', display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
@@ -953,6 +981,7 @@ export default function WayLinePage({ planedevices, onRefresh }) {
|
||||
onChange={(val) => {
|
||||
const find = planedevices.find(i => i.device_sn === val);
|
||||
setWaylineDrone(find || null);
|
||||
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -1096,6 +1125,7 @@ export default function WayLinePage({ planedevices, onRefresh }) {
|
||||
onChange={(val) => {
|
||||
const find = planedevices.find(i => i.device_sn === val);
|
||||
setBoardDrone(find || null);
|
||||
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
@@ -1461,10 +1491,10 @@ export default function WayLinePage({ planedevices, onRefresh }) {
|
||||
<div>
|
||||
<div style={{ marginBottom: 8 }}><Text strong>返航高度 (m):</Text></div>
|
||||
<InputNumber
|
||||
min={20}
|
||||
min={10}
|
||||
max={500}
|
||||
value={tempRthAltitude}
|
||||
onChange={val => setTempRthAltitude(val || 100)}
|
||||
onChange={val => setTempRthAltitude(val || 20)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
|
||||
|
||||
84
src/components/droneplayer.js
Normal file
84
src/components/droneplayer.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import VERTC, {
|
||||
RoomProfileType,
|
||||
StreamIndex,
|
||||
} from "@volcengine/rtc";
|
||||
|
||||
export class DroneRTC {
|
||||
constructor(appId) {
|
||||
if (!appId) {
|
||||
throw new Error("appId is required");
|
||||
}
|
||||
this.engine = VERTC.createEngine(appId);
|
||||
this.remoteUserId = null;
|
||||
this.isDestroyed = false;
|
||||
}
|
||||
|
||||
async joinRoom(container, roomId, token, userId) {
|
||||
// 核心修复:engine 为空 / 已销毁,直接退出
|
||||
if (!this.engine || this.isDestroyed) return;
|
||||
|
||||
await this.leaveRoom();
|
||||
|
||||
// 安全清理监听
|
||||
try {
|
||||
this.engine.off(VERTC.events.onUserPublishStream);
|
||||
} catch (e) { }
|
||||
|
||||
// 监听用户发布流
|
||||
this.engine.on(VERTC.events.onUserPublishStream, async (e) => {
|
||||
this.remoteUserId = e.userId;
|
||||
if (!container) return;
|
||||
|
||||
try {
|
||||
this.engine.setRemoteVideoPlayer(StreamIndex.STREAM_INDEX_MAIN, {
|
||||
userId: e.userId,
|
||||
renderDom: container,
|
||||
visible: true,
|
||||
});
|
||||
} catch (err) { }
|
||||
});
|
||||
|
||||
// 加入房间
|
||||
await this.engine.joinRoom(
|
||||
token,
|
||||
roomId,
|
||||
{ userId },
|
||||
{
|
||||
isAutoPublish: false,
|
||||
isAutoSubscribeVideo: true,
|
||||
isAutoSubscribeAudio: false,
|
||||
roomProfileType: RoomProfileType.meeting,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async leaveRoom() {
|
||||
if (!this.engine || this.isDestroyed) return;
|
||||
|
||||
try {
|
||||
if (this.remoteUserId) {
|
||||
this.engine.setRemoteVideoPlayer(StreamIndex.STREAM_INDEX_MAIN, {
|
||||
userId: this.remoteUserId,
|
||||
renderDom: null,
|
||||
visible: false,
|
||||
});
|
||||
}
|
||||
await this.engine.leaveRoom();
|
||||
} catch (err) { }
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// 核心修复:防止重复销毁
|
||||
if (this.isDestroyed) return;
|
||||
|
||||
this.isDestroyed = true;
|
||||
this.leaveRoom();
|
||||
|
||||
if (this.engine) {
|
||||
try {
|
||||
this.engine.destroy();
|
||||
} catch (e) { }
|
||||
this.engine = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
import VERTC, {
|
||||
RoomProfileType,
|
||||
StreamIndex,
|
||||
} from "@volcengine/rtc";
|
||||
|
||||
let engine = null;
|
||||
|
||||
let currentRemoteUserId = null;
|
||||
|
||||
let isJoining = false;
|
||||
|
||||
|
||||
// =========================
|
||||
// 创建单例 engine
|
||||
// =========================
|
||||
|
||||
function getEngine(appId) {
|
||||
if (!engine) {
|
||||
engine = VERTC.createEngine(appId);
|
||||
}
|
||||
|
||||
return engine;
|
||||
}
|
||||
|
||||
|
||||
// =========================
|
||||
// 加入房间
|
||||
// =========================
|
||||
|
||||
export async function joinRoom(
|
||||
container,
|
||||
appId,
|
||||
roomId,
|
||||
token,
|
||||
userId
|
||||
) {
|
||||
if (isJoining) {
|
||||
console.warn("正在 joinRoom,跳过");
|
||||
return;
|
||||
}
|
||||
|
||||
isJoining = true;
|
||||
|
||||
try {
|
||||
console.log("joinRoom start");
|
||||
|
||||
// 先离开旧房间
|
||||
await leaveRoom();
|
||||
|
||||
const rtcEngine = getEngine(appId);
|
||||
|
||||
// 防止重复监听
|
||||
rtcEngine.removeAllListeners();
|
||||
|
||||
// 监听远端流
|
||||
rtcEngine.on(
|
||||
VERTC.events.onUserPublishStream,
|
||||
async (e) => {
|
||||
console.log("收到远端流", e);
|
||||
|
||||
currentRemoteUserId = e.userId;
|
||||
|
||||
if (!container) return;
|
||||
|
||||
try {
|
||||
// 清空旧 video
|
||||
container.innerHTML = "";
|
||||
|
||||
// 隐藏本地
|
||||
await rtcEngine.setUserVisibility(false);
|
||||
|
||||
// 播放远端
|
||||
rtcEngine.setRemoteVideoPlayer(
|
||||
StreamIndex.STREAM_INDEX_MAIN,
|
||||
{
|
||||
userId: e.userId,
|
||||
renderDom: container,
|
||||
visible: true,
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 加入房间
|
||||
await rtcEngine.joinRoom(
|
||||
token,
|
||||
roomId,
|
||||
{ userId },
|
||||
{
|
||||
isAutoPublish: false,
|
||||
isAutoSubscribeAudio: false,
|
||||
isAutoSubscribeVideo: true,
|
||||
roomProfileType:
|
||||
RoomProfileType.meeting,
|
||||
}
|
||||
);
|
||||
|
||||
console.log("joinRoom success");
|
||||
} catch (err) {
|
||||
console.error("joinRoom failed", err);
|
||||
} finally {
|
||||
isJoining = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =========================
|
||||
// 离开房间
|
||||
// =========================
|
||||
|
||||
export async function leaveRoom() {
|
||||
if (!engine) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("leaveRoom start");
|
||||
|
||||
try {
|
||||
// 移除事件
|
||||
engine.removeAllListeners();
|
||||
|
||||
// 停止远端渲染
|
||||
if (currentRemoteUserId) {
|
||||
try {
|
||||
engine.setRemoteVideoPlayer(
|
||||
StreamIndex.STREAM_INDEX_MAIN,
|
||||
{
|
||||
userId: currentRemoteUserId,
|
||||
renderDom: null,
|
||||
visible: false,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 离开房间
|
||||
try {
|
||||
await engine.leaveRoom();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
// 等待 SDK 内部释放
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, 500)
|
||||
);
|
||||
|
||||
currentRemoteUserId = null;
|
||||
|
||||
console.log("leaveRoom success");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user