364 lines
15 KiB
TypeScript
364 lines
15 KiB
TypeScript
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); |