Files
web-Iot/src/components/AgrtcRoomRobot.tsx

282 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useRef, useState } from "react";
import AgoraRTC, { IAgoraRTCClient, IRemoteVideoTrack } from "agora-rtc-sdk-ng";
import { Badge, message } from "antd";
AgoraRTC.setLogLevel(2); // 2=只输出warning/error,屏蔽debug日志
const APP_ID = "7887b4db6033410d9ac3264b40ca9c1a";
const TOKEN = "007eJxTYPh6QSj59sUzD3mPNm64dK9n/n1PP64UzrhzdwUeqMUKv1BVYDC3sDBPMklJMjMwNjYxNEixTEw2NjIzSTIxSE60TDZMlOdYldUQyMiQXKbEwAiFID43g4mhbklGUWpiroExAwMAjGAhHQ==";
export default function AgrtcRoomRobot({ CHANNEL }: { CHANNEL: string }) {
const [uid] = useState(() => Number(Math.floor(Math.random() * 100000)));
const [connected, setConnected] = useState(false);
const [joined, setJoined] = useState(false);
const [hasRemoteVideo, setHasRemoteVideo] = 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);
const subscribedUidRef = useRef<number | null>(null);
useEffect(() => {
const client = AgoraRTC.createClient({
mode: "live",
codec: "h264",
});
clientRef.current = client;
client.setClientRole("audience");
// ========= 删掉了 setParameters,Web NG不支持!========
let isUnmounted = false;
let videoWatchDogTimer: NodeJS.Timeout | null = null;
let unpublishTimer: NodeJS.Timeout | null = null;
const clearRemoteVideo = () => {
if (remoteVideoTrackRef.current) {
try {
remoteVideoTrackRef.current.stop();
} catch (e) {
console.warn("stop track error", e);
}
remoteVideoTrackRef.current = null;
}
subscribedUidRef.current = null;
setHasRemoteVideo(false);
if (videoWatchDogTimer) clearInterval(videoWatchDogTimer);
};
const startVideoWatchDog = () => {
if (videoWatchDogTimer) clearInterval(videoWatchDogTimer);
videoWatchDogTimer = setInterval(async () => {
const track = remoteVideoTrackRef.current;
const dom = remoteVideoRef.current;
if (!track || !dom) return;
const videoEl = dom.querySelector("video") as HTMLVideoElement;
if (!videoEl) return;
if (videoEl.readyState < 2) {
console.warn("看门狗检测视频无画面,尝试重新play");
try {
await track.play(dom);
} catch (e) {
console.warn("看门狗play异常", e);
}
}
}, 3000);
};
const playRemoteTrack = async (track: IRemoteVideoTrack) => {
const dom = remoteVideoRef.current;
if (!dom || !track) return;
try {
await track.play(dom);
console.log("远端视频播放成功");
setHasRemoteVideo(true);
startVideoWatchDog();
} catch (err: any) {
if (err.name === "AbortError") {
console.warn("播放请求被中断 AbortError,忽略", err);
return;
}
console.error("视频播放失败", err);
message.error("视频播放失败");
}
};
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 && subscribedUidRef.current !== remoteUser.uid) {
await client.subscribe(remoteUser, "video");
remoteVideoTrackRef.current = remoteUser.videoTrack;
subscribedUidRef.current = remoteUser.uid;
await playRemoteTrack(remoteUser.videoTrack!);
}
}
} 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);
if (mediaType !== "video") return;
if (subscribedUidRef.current === user.uid) {
console.log("该用户已订阅,跳过");
return;
}
// 取消待执行的清空定时器
if (unpublishTimer) clearTimeout(unpublishTimer);
await client.subscribe(user, mediaType);
remoteVideoTrackRef.current = user.videoTrack;
subscribedUidRef.current = user.uid;
await playRemoteTrack(user.videoTrack);
};
const onUserUnpublished = (user: any, mediaType: "video" | "audio") => {
if (isUnmounted) return;
console.log("user-unpublished", user.uid, mediaType, subscribedUidRef.current);
if (mediaType === "video" && subscribedUidRef.current === user.uid) {
if (unpublishTimer) clearTimeout(unpublishTimer);
// 延迟2s清空,短闪断自动恢复不黑屏
unpublishTimer = setTimeout(() => {
clearRemoteVideo();
}, 2000);
}
};
const onUserLeft = (user: any) => {
if (isUnmounted) return;
console.log("user-left", user.uid);
if (subscribedUidRef.current === user.uid) {
if (unpublishTimer) clearTimeout(unpublishTimer);
clearRemoteVideo();
}
};
const onUserJoined = (user: any) => {
if (!isUnmounted) console.log("远端用户进入频道:", user.uid);
};
const onTokenWillExpire = () => {
console.warn("token即将过期,请更新token");
message.warning("Token即将过期,视频可能很快断开");
};
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 && subscribedUidRef.current !== remoteUser.uid) {
await client.subscribe(remoteUser, "video");
remoteVideoTrackRef.current = remoteUser.videoTrack;
subscribedUidRef.current = remoteUser.uid;
await playRemoteTrack(remoteUser.videoTrack!);
}
}
} catch (err: any) {
if (err?.code === "OPERATION_ABORTED") {
console.log("join操作被取消,忽略");
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);
if (unpublishTimer) clearTimeout(unpublishTimer);
if (videoWatchDogTimer) clearInterval(videoWatchDogTimer);
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;
};
}, [CHANNEL]);
return (
<div style={{ width: "100%", height: "95%", display: "flex", flexDirection: "column", borderRadius: 8, overflow: "hidden", background: "#000" }}>
<div style={{ display: "flex", justifyContent: "end", alignItems: "center", gap: 12, padding: "4px 8px", background: "rgba(255,255,255,0.08)", flexShrink: 0 }}>
<Badge
color={connected ? "green" : "red"}
text={connected ? "RTC已连接" : "RTC未连接"}
styles={{ content: { color: '#888' } }}
/>
<Badge
color={joined ? "green" : "orange"}
text={joined ? "已加入频道" : "未加入频道"}
styles={{ content: { color: '#888' } }}
/>
<span style={{ fontSize: 11, color: "#888" }}>频道:{CHANNEL}</span>
</div>
<div style={{ flex: 1, width: "100%", minHeight: 0, position: "relative" }}>
{!hasRemoteVideo && (
<div style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#aaa",
fontSize: 14,
background: "#000",
zIndex: 2
}}>
视频加载中,等待远端推流...
</div>
)}
<div ref={remoteVideoRef} style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
zIndex: 1
}} />
</div>
</div >
);
}