无人机组件 终于可以显示两个视频了!!!!

This commit is contained in:
mmc
2026-05-28 15:17:00 +08:00
parent 834f93b947
commit 1ca393472d
9 changed files with 317 additions and 353 deletions

View File

@@ -224,3 +224,12 @@ export function getAllDevice(data) {
})
}
export function getPlaneDeviceListAll() {
return request({
url: '/iot/UAV/getProjectDeviceList',
method: 'get',
})
}

View File

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

View File

@@ -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%",

View File

@@ -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}

View File

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

View File

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

View File

@@ -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%' }}
/>

View 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;
}
}
}

View File

@@ -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);
}
}