import AgoraRTC, { ICameraVideoTrack, IMicrophoneAudioTrack } from 'agora-rtc-sdk-ng'; import { memo, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { useGetAgoraChannelsQuery, useGetAgoraStatusQuery, useGenerateAgoraTokenMutation } from '../../app/services/server'; import { updateChannelVisibleAside } from '../../app/slices/footprint'; import { addVoiceMember, removeVoiceMember, updateConnectionState, updateDeafenStatus, updateMuteStatus, updateVoicingInfo, updateVoicingMember, updateVoicingNetworkQuality, upsertVoiceList } from '../../app/slices/voice'; import { useAppSelector } from '../../app/store'; import AudioJoin from '../../assets/join.wav'; import { playAgoraVideo } from '../utils'; import { ShareScreenTrack } from '../../types/global'; AgoraRTC.setLogLevel(process.env.NODE_ENV === 'development' ? 0 : 4); window.VOICE_TRACK_MAP = window.VOICE_TRACK_MAP ?? {}; window.VIDEO_TRACK_MAP = window.VIDEO_TRACK_MAP ?? {}; // let tmpUids: number[] = []; const Voice = () => { const { data: enabled } = useGetAgoraStatusQuery(); useGetAgoraChannelsQuery({ page_no: 0, page_size: 100 }, { skip: !enabled || !navigator.onLine, pollingInterval: 5000 }); const dispatch = useDispatch(); useEffect(() => { const initializeAgoraClient = async () => { // 创建agora客户端实例 const agoraEngine = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" }); // 无论频道内是否有人说话,都会每两秒返回提示音量 agoraEngine.enableAudioVolumeIndicator(); // Listen for the "user-published" event to retrieve an AgoraRTCRemoteUser object. agoraEngine.on("user-published", async (user, mediaType) => { // Subscribe to the remote user when the SDK triggers the "user-published" event. await agoraEngine.subscribe(user, mediaType); console.log(user, " has published at the channel"); if (mediaType == "audio") { // 播放远端音频 user.audioTrack?.play(); window.VOICE_TRACK_MAP[+user.uid] = user.audioTrack; } if (mediaType == "video") { const label = user.videoTrack?.getMediaStreamTrack()?.label; console.log("labell", label); if (label?.includes("screen")) { // 远端用户共享屏幕 // const screenTrack = user.videoTrack as ShareScreenTrack; // screenTrack.on("track-ended", () => { // // 远端用户停止共享屏幕 dispatch(updateVoicingMember({ uid: +user.uid, info: { shareScreen: true } })); } window.VIDEO_TRACK_MAP[+user.uid] = user.videoTrack; playAgoraVideo(+user.uid); } agoraEngine.on("user-unpublished", (user) => { if (!user.hasAudio) { // 远端用户取消了音频(muted) dispatch(updateVoicingMember({ uid: +user.uid, info: { muted: true } })); } if (!user.hasVideo) { // 远端用户取消了视频 dispatch(updateVoicingMember({ uid: +user.uid, info: { video: false, shareScreen: false } })); // 注销本地视频变量 window.VIDEO_TRACK_MAP[+user.uid] = null; // 关闭视频后,需要把视频的高度设置回去 const playerEle = document.querySelector(`#CAMERA_${user.uid}`) as HTMLElement; playerEle.classList.remove("h-[120px]"); } }); //remote user leave agoraEngine.on("user-left", (user, reason) => { console.log(user, "has left the channel"); switch (reason) { case "Quit": case "ServerTimeOut": { dispatch(removeVoiceMember(+user.uid as number)); // clear tracks window.VOICE_TRACK_MAP[+user.uid] = null; window.VIDEO_TRACK_MAP[+user.uid] = null; } break; default: break; } }); // 报告频道内正在说话的远端用户及其音量的回调。 agoraEngine.on("volume-indicator", (vols) => { vols.forEach((vol, index) => { console.log(`${index} UID ${vol.uid} Level ${vol.level}`); const { uid, level } = vol; dispatch(updateVoicingMember({ uid: +uid, info: { speakingVolume: level } })); }); }); // 信号强度 agoraEngine.on("network-quality", (qlt) => { const { downlinkNetworkQuality } = qlt; dispatch(updateVoicingNetworkQuality(downlinkNetworkQuality)); }); // 连接状态有变化 agoraEngine.on("connection-state-change", (state, prevState, reason) => { console.log("connection-state-change", state, prevState, reason); dispatch(updateConnectionState(state)); }); // 用户状态变化 agoraEngine.on("user-info-updated", (uid, msg) => { console.log("user-info-updated", uid, msg); switch (msg) { case "mute-audio": // todo dispatch(updateVoicingMember({ uid: +uid, info: { muted: true } })); break; case "unmute-audio": // todo dispatch(updateVoicingMember({ uid: +uid, info: { muted: false } })); break; default: break; } }); }); // 有新用户加入 agoraEngine.on("user-joined", async (user) => { console.log(user.uid, agoraEngine.channelName, " has joined the channel"); dispatch(addVoiceMember(+user.uid)); }); window.VOICE_CLIENT = agoraEngine; }; const handlePageUnload = (evt: BeforeUnloadEvent) => { console.log("unload"); if (window.VOICE_CLIENT?.connectionState === "CONNECTED") { evt.preventDefault(); return (evt.returnValue = ""); } }; window.addEventListener("beforeunload", handlePageUnload, { capture: true }); if (!window.VOICE_CLIENT) { initializeAgoraClient(); } return () => { window.removeEventListener("beforeunload", handlePageUnload, { capture: true }); }; }, []); return null; }; type VoiceProps = { id: number, context?: "channel" | "dm" } const audioJoin = new Audio(AudioJoin); const useVoice = ({ id, context = "channel" }: VoiceProps) => { const dispatch = useDispatch(); const { voicingInfo, loginUid } = useAppSelector(store => { return { loginUid: store.authData.user?.uid ?? 0, voicingInfo: store.voice.voicing }; }); const [generateToken] = useGenerateAgoraTokenMutation(); // const [joining, setJoining] = useState(false); const joinVoice = async () => { // setJoining(true); dispatch(updateVoicingInfo({ id, context, joining: true })); const resp = await generateToken(context == "channel" ? { gid: id } : { uid: id }); if ('error' in resp) { console.error("generate agora token error"); dispatch(updateVoicingInfo({ joining: false, id, context, })); } else { console.table(resp.data); const { channel_name, app_id, agora_token, uid } = resp.data; if (window.VOICE_CLIENT) { await window.VOICE_CLIENT.join(app_id, channel_name, agora_token, uid); // Create a local audio track from the microphone audio. const localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack(); // Publish the local audio track in the channel. await window.VOICE_CLIENT.publish(localAudioTrack); // play the join audio audioJoin.play(); console.log("Publish success!,joined the channel"); dispatch(updateVoicingInfo({ deafen: false, muted: false, joining: false, id, context, })); // 把自己加进去 dispatch(addVoiceMember(uid)); // 放到全局变量里 window.VOICE_TRACK_MAP[loginUid] = localAudioTrack; } } // setJoining(false); }; const openCamera = async () => { const localVideoTrack = await AgoraRTC.createCameraVideoTrack(); await window.VOICE_CLIENT?.publish(localVideoTrack); dispatch(updateVoicingInfo({ video: true, id, context, })); // 放到全局变量里 window.VIDEO_TRACK_MAP[loginUid] = localVideoTrack; playAgoraVideo(loginUid); }; const closeCamera = async () => { const localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ICameraVideoTrack; if (localVideoTrack) { await window.VOICE_CLIENT?.unpublish(localVideoTrack); localVideoTrack.close(); window.VIDEO_TRACK_MAP[loginUid] = null; // 关闭视频后,需要把视频的高度设置回去 const playerEle = document.querySelector(`#CAMERA_${loginUid}`) as HTMLElement; playerEle.classList.remove("h-[120px]"); dispatch(updateVoicingInfo({ video: false, id, context, })); } }; const stopShareScreen = async () => { let localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ShareScreenTrack | null; if (!localVideoTrack) return; await window.VOICE_CLIENT?.unpublish(localVideoTrack); if ("close" in localVideoTrack) { localVideoTrack.close(); } else { localVideoTrack[0].close(); // localVideoTrack[0]=null } // localVideoTrack.close(); window.VIDEO_TRACK_MAP[loginUid] = null; // 关闭视频后,需要把视频的高度设置回去 const playerEle = document.querySelector(`#CAMERA_${loginUid}`) as HTMLElement; playerEle.classList.remove("h-[120px]"); dispatch(updateVoicingInfo({ video: false, shareScreen: false, id, context, })); }; const startShareScreen = async () => { try { const localVideoTrack = await AgoraRTC.createScreenVideoTrack({ electronScreenSourceId: "share_screen", selfBrowserSurface: "exclude", // 配置屏幕共享编码参数,详情请查看 API 文档。 encoderConfig: "1080p_1", // 设置视频传输优化策略为清晰优先或流畅优先。 optimizationMode: "detail", }); // 取消正在进行的视频 await closeCamera(); await window.VOICE_CLIENT?.publish(localVideoTrack); dispatch(updateVoicingInfo({ video: false, shareScreen: true, id, context, })); // 放到全局变量里 window.VIDEO_TRACK_MAP[loginUid] = localVideoTrack; playAgoraVideo(loginUid); // 监听屏幕共享结束事件 if ("close" in localVideoTrack) { localVideoTrack.getMediaStreamTrack().onended = () => { stopShareScreen(); }; } } catch (error) { console.log("start share screen error", error); } }; const leave = async () => { const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack; const localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ICameraVideoTrack; if (window.VOICE_CLIENT) { await window.VOICE_CLIENT.leave(); dispatch(updateVoicingInfo(null)); if (localAudioTrack) { localAudioTrack.close(); window.VOICE_TRACK_MAP[loginUid] = null; } if (localVideoTrack) { localVideoTrack.close(); window.VIDEO_TRACK_MAP[loginUid] = null; } if (context == "channel") { dispatch(updateChannelVisibleAside({ id, aside: null })); // 即时更新对应的活跃列表信息 dispatch(upsertVoiceList({ id, context, memberCount: 0 })); } } }; const setMute = (mute: boolean) => { const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack; if (!localAudioTrack) return; localAudioTrack.setMuted(mute); if (mute == false && voicingInfo?.deafen) { // 远端音频,恢复原音 Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => { audioTrack?.setVolume(100); }); dispatch(updateDeafenStatus(false)); } dispatch(updateMuteStatus(mute)); }; const setDeafen = (deafen: boolean) => { const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack; if (!localAudioTrack) return; if (deafen) { localAudioTrack.setMuted(true); // 远端音频,全部静音 Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => { audioTrack?.setVolume(0); }); } else { localAudioTrack.setMuted(false); // 远端音频,恢复原音 Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => { audioTrack?.setVolume(100); }); } dispatch(updateDeafenStatus(deafen)); }; const joinedAtThisContext = voicingInfo ? (voicingInfo.id == id && voicingInfo.context == context) : false; return { setMute, setDeafen, leave, // canVoice, voicingInfo, joining: voicingInfo ? voicingInfo.joining : undefined, joinedAtThisContext, joined: !!voicingInfo, joinVoice, openCamera, closeCamera, startShareScreen, stopShareScreen, }; }; export { useVoice }; export default memo(Voice);