From 23d8fb757c8ede164813016351d3c48b55647083 Mon Sep 17 00:00:00 2001 From: Tristan Yang Date: Mon, 24 Apr 2023 20:45:06 +0800 Subject: [PATCH] refactor: video call --- .../listener.middleware/handler.footprint.ts | 11 ++- src/app/services/server.ts | 17 +++-- src/app/slices/footprint.ts | 23 +++++-- src/common/component/Voice.tsx | 69 ++++++++++--------- src/common/utils.tsx | 13 ++++ src/routes/chat/DMChat/index.tsx | 5 ++ src/routes/chat/VoiceChat/VoiceManagement.tsx | 10 ++- src/routes/chat/VoiceChat/index.tsx | 25 +++---- src/routes/settingChannel/index.tsx | 2 +- 9 files changed, 114 insertions(+), 61 deletions(-) diff --git a/src/app/listener.middleware/handler.footprint.ts b/src/app/listener.middleware/handler.footprint.ts index 4b1572ce..11f07162 100644 --- a/src/app/listener.middleware/handler.footprint.ts +++ b/src/app/listener.middleware/handler.footprint.ts @@ -71,7 +71,16 @@ export default async function handler({ operation, data = {}, payload }: Params) break; case "updateChannelVisibleAside": { - await table?.setItem("channelAsides", data.channelAsides); + if (payload.aside !== "voice") { + await table?.setItem("channelAsides", data.channelAsides); + } + } + break; + case "updateDMVisibleAside": + { + if (payload.aside !== "voice") { + await table?.setItem("dmAsides", data.dmAsides); + } } break; default: diff --git a/src/app/services/server.ts b/src/app/services/server.ts index d40e6a66..99617eea 100644 --- a/src/app/services/server.ts +++ b/src/app/services/server.ts @@ -124,11 +124,12 @@ export const serverApi = createApi({ const { success } = resp; if (success) { const arr = resp.data.channels.map(data => { - const [id] = data.channel_name.split(":").slice(-1); + const [type, id] = data.channel_name.split(":").slice(-2); const count = data.user_count; + const context = type === "group" ? "channel" as const : "dm" as const; return { id: +id, - context: "channel" as const, + context, memberCount: count }; }); @@ -149,9 +150,11 @@ export const serverApi = createApi({ getAgoraStatus: builder.query({ query: () => ({ url: `/admin/agora/enabled` }) }), - getAgoraToken: builder.query({ - query: (id) => ({ - url: `group/${id}/agora_token`, + generateAgoraToken: builder.mutation({ + query: (data) => ({ + url: `/admin/agora/token`, + method: "POST", + body: data }) }), getSystemCommon: builder.query({ @@ -396,11 +399,11 @@ export const { useSendMessageByBotMutation, useUpdateFrontendUrlMutation, useGetFrontendUrlQuery, - useLazyGetAgoraTokenQuery, useGetAgoraConfigQuery, useGetAgoraStatusQuery, useGetAgoraChannelsQuery, useUpdateSystemCommonMutation, useLazyGetSystemCommonQuery, - useGetSystemCommonQuery + useGetSystemCommonQuery, + useGenerateAgoraTokenMutation } = serverApi; diff --git a/src/app/slices/footprint.ts b/src/app/slices/footprint.ts index 0479fc37..0241d64a 100644 --- a/src/app/slices/footprint.ts +++ b/src/app/slices/footprint.ts @@ -5,6 +5,7 @@ import { AutoDeleteMessageSettingDTO, AutoDeleteMsgForGroup, AutoDeleteMsgForUse import { resetAuthData } from "./auth.data"; type ChannelAside = "members" | "voice" | null; +type DMAside = "voice" | null; export interface State { og: { [url: string]: OG } usersVersion: number; @@ -18,6 +19,7 @@ export interface State { autoDeleteMsgUsers: AutoDeleteMsgForUser[]; autoDeleteMsgChannels: AutoDeleteMsgForGroup[]; channelAsides: { [cid: number]: ChannelAside }; + dmAsides: { [uid: number]: DMAside }; } @@ -33,7 +35,8 @@ const initialState: State = { muteChannels: {}, autoDeleteMsgUsers: [], autoDeleteMsgChannels: [], - channelAsides: {} + channelAsides: {}, + dmAsides: {} }; const footprintSlice = createSlice({ @@ -56,7 +59,8 @@ const footprintSlice = createSlice({ muteChannels = {}, autoDeleteMsgUsers = [], autoDeleteMsgChannels = [], - channelAsides = {} + channelAsides = {}, + dmAsides = {} } = action.payload; return { og, @@ -70,7 +74,8 @@ const footprintSlice = createSlice({ muteChannels, autoDeleteMsgUsers, autoDeleteMsgChannels, - channelAsides + channelAsides, + dmAsides }; }, updateUsersVersion(state, action: PayloadAction) { @@ -189,6 +194,10 @@ const footprintSlice = createSlice({ const { id, aside } = action.payload; state.channelAsides[id] = aside; }, + updateDMVisibleAside(state, action: PayloadAction<{ id: number, aside: DMAside }>) { + const { id, aside } = action.payload; + state.dmAsides[id] = aside; + }, }, extraReducers: (builder) => { builder.addCase(resetAuthData, (state) => { @@ -198,6 +207,11 @@ const footprintSlice = createSlice({ state.channelAsides[+channel_id] = null; } }); + Object.keys(state.dmAsides).forEach((uid: string) => { + if (state.dmAsides[+uid] === "voice") { + state.dmAsides[+uid] = null; + } + }); }); } }); @@ -213,7 +227,8 @@ export const { updateAutoDeleteSetting, updateHistoryMark, upsertOG, - updateChannelVisibleAside + updateChannelVisibleAside, + updateDMVisibleAside } = footprintSlice.actions; export default footprintSlice.reducer; diff --git a/src/common/component/Voice.tsx b/src/common/component/Voice.tsx index 7477dd76..e73d43ba 100644 --- a/src/common/component/Voice.tsx +++ b/src/common/component/Voice.tsx @@ -1,11 +1,12 @@ import AgoraRTC, { ICameraVideoTrack, IMicrophoneAudioTrack } from 'agora-rtc-sdk-ng'; import { memo, useEffect } from 'react'; import { useDispatch } from 'react-redux'; -import { useGetAgoraChannelsQuery, useGetAgoraStatusQuery, useLazyGetAgoraTokenQuery } from '../../app/services/server'; +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 { compareVersion } from '../utils'; // type Props = {} AgoraRTC.setLogLevel(process.env.NODE_ENV === 'development' ? 0 : 4); @@ -42,12 +43,8 @@ const Voice = () => { window.VOICE_TRACK_MAP[+user.uid] = user.audioTrack; } if (mediaType == "video") { - const playerEle = document.querySelector(`#CAMERA_${user.uid}`) as HTMLElement; - if (playerEle) { - playerEle.classList.add("h-[120px]"); - user.videoTrack?.play(playerEle); - } window.VIDEO_TRACK_MAP[+user.uid] = user.videoTrack; + playAgoraVideo(+user.uid); } agoraEngine.on("user-unpublished", (user) => { if (!user.hasAudio) { @@ -112,14 +109,14 @@ const Voice = () => { }); // 有新用户加入 agoraEngine.on("user-joined", async (user) => { - console.log(user.uid, !!localAudioTrack, agoraEngine.channelName, " has joined the channel"); + 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 (localAudioTrack) { + if (window.VOICE_CLIENT?.connectionState === "CONNECTED") { evt.preventDefault(); return (evt.returnValue = ""); } @@ -131,14 +128,15 @@ const Voice = () => { return () => { window.removeEventListener("beforeunload", handlePageUnload, { capture: true }); + }; }, []); return null; }; -let localAudioTrack: IMicrophoneAudioTrack | null = null; -let localVideoTrack: ICameraVideoTrack | null = null; +// let localAudioTrack: IMicrophoneAudioTrack | null = null; +// let localVideoTrack: ICameraVideoTrack | null = null; type VoiceProps = { id: number, context?: "channel" | "dm" @@ -148,11 +146,11 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { const dispatch = useDispatch(); const { voicingInfo, loginUid } = useAppSelector(store => { return { - loginUid: store.authData.user?.uid, + loginUid: store.authData.user?.uid ?? 0, voicingInfo: store.voice.voicing }; }); - const [generateToken] = useLazyGetAgoraTokenQuery(); + const [generateToken] = useGenerateAgoraTokenMutation(); // const [joining, setJoining] = useState(false); const joinVoice = async () => { // setJoining(true); @@ -161,14 +159,21 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { context, joining: true })); - const { isError, data } = await generateToken(id); - if (!isError && data) { - const { channel_name, app_id, agora_token, uid } = data; + 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); - console.table(data); // Create a local audio track from the microphone audio. - localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack(); + const localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack(); // Publish the local audio track in the channel. await window.VOICE_CLIENT.publish(localAudioTrack); // play the join audio @@ -183,37 +188,31 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { })); // 把自己加进去 dispatch(addVoiceMember(uid)); + // 放到全局变量里 + window.VOICE_TRACK_MAP[loginUid] = localAudioTrack; } - } else { - console.error("generate agora token error"); - dispatch(updateVoicingInfo({ - joining: false, - id, - context, - })); } // setJoining(false); }; const openCamera = async () => { - localVideoTrack = await AgoraRTC.createCameraVideoTrack(); + const localVideoTrack = await AgoraRTC.createCameraVideoTrack(); await window.VOICE_CLIENT?.publish(localVideoTrack); - const playerEle = document.querySelector(`#CAMERA_${loginUid}`) as HTMLElement; - if (playerEle) { - playerEle.classList.add("h-[120px]"); - localVideoTrack?.play(playerEle); - } 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(); - localVideoTrack = null; + window.VIDEO_TRACK_MAP[loginUid] = null; // 关闭视频后,需要把视频的高度设置回去 const playerEle = document.querySelector(`#CAMERA_${loginUid}`) as HTMLElement; playerEle.classList.remove("h-[120px]"); @@ -226,12 +225,14 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { }; 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 && localAudioTrack) { localAudioTrack.close(); - localAudioTrack = null; + window.VOICE_TRACK_MAP[loginUid] = null; if (localVideoTrack) { localVideoTrack.close(); - localVideoTrack = null; + window.VIDEO_TRACK_MAP[loginUid] = null; } await window.VOICE_CLIENT.leave(); dispatch(updateVoicingInfo(null)); @@ -249,12 +250,14 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { } }; const setMute = (mute: boolean) => { + const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack; if (localAudioTrack) { localAudioTrack.setMuted(mute); dispatch(updateMuteStatus(mute)); } }; const setDeafen = (deafen: boolean) => { + const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack; if (localAudioTrack) { if (deafen) { localAudioTrack.setMuted(true); diff --git a/src/common/utils.tsx b/src/common/utils.tsx index 61c393b0..046f5648 100644 --- a/src/common/utils.tsx +++ b/src/common/utils.tsx @@ -1,5 +1,6 @@ import dayjs from "dayjs"; import BASE_URL, { FILE_IMAGE_SIZE, ContentTypes, KEY_TOKEN, KEY_REFRESH_TOKEN, KEY_EXPIRE } from "../app/config"; +import { ICameraVideoTrack } from "agora-rtc-sdk-ng"; import IconPdf from "../assets/icons/file.pdf.svg"; import IconAudio from "../assets/icons/file.audio.svg"; import IconVideo from "../assets/icons/file.video.svg"; @@ -371,4 +372,16 @@ export const fromNowTime = (ts?: number) => { if (!ts) return null; const currTS = + new Date(); return dayjs(ts > currTS ? currTS : ts).fromNow(); +}; +export const playAgoraVideo = (uid: number, videoTrack?: ICameraVideoTrack | null) => { + if (!videoTrack && !window.VIDEO_TRACK_MAP[uid]) return; + const playerEle = document.querySelector(`#CAMERA_${uid}`) as HTMLElement; + if (playerEle) { + playerEle.classList.add("h-[120px]"); + if (videoTrack) { + videoTrack.play(playerEle); + } else { + window.VIDEO_TRACK_MAP[uid]?.play(playerEle); + } + } }; \ No newline at end of file diff --git a/src/routes/chat/DMChat/index.tsx b/src/routes/chat/DMChat/index.tsx index 2d89ec94..6ae7a7f5 100644 --- a/src/routes/chat/DMChat/index.tsx +++ b/src/routes/chat/DMChat/index.tsx @@ -8,6 +8,8 @@ import User from "../../../common/component/User"; import Layout from "../Layout"; import { useAppSelector } from "../../../app/store"; import GoBackNav from "../../../common/component/GoBackNav"; +import ServerVersionChecker from "../../../common/component/ServerVersionChecker"; +import VoiceChat from "../VoiceChat"; type Props = { uid: number; dropFiles?: File[]; @@ -33,6 +35,9 @@ const DMChat: FC = ({ uid = 0, dropFiles }) => { dropFiles={dropFiles} aside={
    + + + { + const ids = voicingMembers.ids; + ids.forEach(id => { + playAgoraVideo(id); + }); + }, [voicingMembers.ids]); + if (!info) return null; const { deafen, muted, video } = info; const nameClass = clsx(`text-sm text-gray-500 max-w-[120px] truncate font-semibold dark:text-white`); diff --git a/src/routes/chat/VoiceChat/index.tsx b/src/routes/chat/VoiceChat/index.tsx index b00aee60..1d4af9f9 100644 --- a/src/routes/chat/VoiceChat/index.tsx +++ b/src/routes/chat/VoiceChat/index.tsx @@ -3,7 +3,7 @@ // import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; -import { updateChannelVisibleAside } from '../../../app/slices/footprint'; +import { updateChannelVisibleAside, updateDMVisibleAside } from '../../../app/slices/footprint'; import { useAppSelector } from '../../../app/store'; import IconHeadphone from '../../../assets/icons/headphone.svg'; import Tooltip from '../../../common/component/Tooltip'; @@ -29,13 +29,11 @@ const VoiceChat = ({ id, context = "channel" }: Props) => { const { data: enabled } = useGetAgoraStatusQuery(); const { t } = useTranslation("chat"); const toggleDashboard = () => { - if (context == "channel") { - dispatch(updateChannelVisibleAside({ - id, - aside: visibleAside == "voice" ? null : "voice" - })); - } - // todo DM + const data = { + id, + aside: visibleAside == "voice" ? null : "voice" as const + }; + dispatch(context == "channel" ? updateChannelVisibleAside(data) : updateDMVisibleAside(data)); }; const handleJoin = () => { if (joining || joined) { @@ -43,12 +41,11 @@ const VoiceChat = ({ id, context = "channel" }: Props) => { return; } joinVoice(); - if (context == "channel") { - dispatch(updateChannelVisibleAside({ - id, - aside: "voice" - })); - } + const data = { + id, + aside: "voice" as const + }; + dispatch(context == "channel" ? updateChannelVisibleAside(data) : updateDMVisibleAside(data)); }; if (!loginUser || !enabled) return null; const visible = visibleAside == "voice"; diff --git a/src/routes/settingChannel/index.tsx b/src/routes/settingChannel/index.tsx index 4884215d..a41f6961 100644 --- a/src/routes/settingChannel/index.tsx +++ b/src/routes/settingChannel/index.tsx @@ -50,7 +50,7 @@ export default function ChannelSetting() { pathPrefix={`/setting/channel/${cid}`} nav={currNav} closeModal={close} - title="Channel Setting" + title="Channel Settings" navs={navs} dangers={[ canLeave && {