282 lines
9.1 KiB
TypeScript
282 lines
9.1 KiB
TypeScript
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 >
|
||
);
|
||
}
|