refactor: video call

This commit is contained in:
Tristan Yang
2023-04-24 20:45:06 +08:00
parent adbd244292
commit 23d8fb757c
9 changed files with 114 additions and 61 deletions
@@ -71,7 +71,16 @@ export default async function handler({ operation, data = {}, payload }: Params)
break; break;
case "updateChannelVisibleAside": 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; break;
default: default:
+10 -7
View File
@@ -124,11 +124,12 @@ export const serverApi = createApi({
const { success } = resp; const { success } = resp;
if (success) { if (success) {
const arr = resp.data.channels.map(data => { 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 count = data.user_count;
const context = type === "group" ? "channel" as const : "dm" as const;
return { return {
id: +id, id: +id,
context: "channel" as const, context,
memberCount: count memberCount: count
}; };
}); });
@@ -149,9 +150,11 @@ export const serverApi = createApi({
getAgoraStatus: builder.query<boolean, void>({ getAgoraStatus: builder.query<boolean, void>({
query: () => ({ url: `/admin/agora/enabled` }) query: () => ({ url: `/admin/agora/enabled` })
}), }),
getAgoraToken: builder.query<AgoraTokenResponse, number>({ generateAgoraToken: builder.mutation<AgoraTokenResponse, { uid: number } | { gid: number }>({
query: (id) => ({ query: (data) => ({
url: `group/${id}/agora_token`, url: `/admin/agora/token`,
method: "POST",
body: data
}) })
}), }),
getSystemCommon: builder.query<SystemCommon, void>({ getSystemCommon: builder.query<SystemCommon, void>({
@@ -396,11 +399,11 @@ export const {
useSendMessageByBotMutation, useSendMessageByBotMutation,
useUpdateFrontendUrlMutation, useUpdateFrontendUrlMutation,
useGetFrontendUrlQuery, useGetFrontendUrlQuery,
useLazyGetAgoraTokenQuery,
useGetAgoraConfigQuery, useGetAgoraConfigQuery,
useGetAgoraStatusQuery, useGetAgoraStatusQuery,
useGetAgoraChannelsQuery, useGetAgoraChannelsQuery,
useUpdateSystemCommonMutation, useUpdateSystemCommonMutation,
useLazyGetSystemCommonQuery, useLazyGetSystemCommonQuery,
useGetSystemCommonQuery useGetSystemCommonQuery,
useGenerateAgoraTokenMutation
} = serverApi; } = serverApi;
+19 -4
View File
@@ -5,6 +5,7 @@ import { AutoDeleteMessageSettingDTO, AutoDeleteMsgForGroup, AutoDeleteMsgForUse
import { resetAuthData } from "./auth.data"; import { resetAuthData } from "./auth.data";
type ChannelAside = "members" | "voice" | null; type ChannelAside = "members" | "voice" | null;
type DMAside = "voice" | null;
export interface State { export interface State {
og: { [url: string]: OG } og: { [url: string]: OG }
usersVersion: number; usersVersion: number;
@@ -18,6 +19,7 @@ export interface State {
autoDeleteMsgUsers: AutoDeleteMsgForUser[]; autoDeleteMsgUsers: AutoDeleteMsgForUser[];
autoDeleteMsgChannels: AutoDeleteMsgForGroup[]; autoDeleteMsgChannels: AutoDeleteMsgForGroup[];
channelAsides: { [cid: number]: ChannelAside }; channelAsides: { [cid: number]: ChannelAside };
dmAsides: { [uid: number]: DMAside };
} }
@@ -33,7 +35,8 @@ const initialState: State = {
muteChannels: {}, muteChannels: {},
autoDeleteMsgUsers: [], autoDeleteMsgUsers: [],
autoDeleteMsgChannels: [], autoDeleteMsgChannels: [],
channelAsides: {} channelAsides: {},
dmAsides: {}
}; };
const footprintSlice = createSlice({ const footprintSlice = createSlice({
@@ -56,7 +59,8 @@ const footprintSlice = createSlice({
muteChannels = {}, muteChannels = {},
autoDeleteMsgUsers = [], autoDeleteMsgUsers = [],
autoDeleteMsgChannels = [], autoDeleteMsgChannels = [],
channelAsides = {} channelAsides = {},
dmAsides = {}
} = action.payload; } = action.payload;
return { return {
og, og,
@@ -70,7 +74,8 @@ const footprintSlice = createSlice({
muteChannels, muteChannels,
autoDeleteMsgUsers, autoDeleteMsgUsers,
autoDeleteMsgChannels, autoDeleteMsgChannels,
channelAsides channelAsides,
dmAsides
}; };
}, },
updateUsersVersion(state, action: PayloadAction<number>) { updateUsersVersion(state, action: PayloadAction<number>) {
@@ -189,6 +194,10 @@ const footprintSlice = createSlice({
const { id, aside } = action.payload; const { id, aside } = action.payload;
state.channelAsides[id] = aside; state.channelAsides[id] = aside;
}, },
updateDMVisibleAside(state, action: PayloadAction<{ id: number, aside: DMAside }>) {
const { id, aside } = action.payload;
state.dmAsides[id] = aside;
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(resetAuthData, (state) => { builder.addCase(resetAuthData, (state) => {
@@ -198,6 +207,11 @@ const footprintSlice = createSlice({
state.channelAsides[+channel_id] = null; 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, updateAutoDeleteSetting,
updateHistoryMark, updateHistoryMark,
upsertOG, upsertOG,
updateChannelVisibleAside updateChannelVisibleAside,
updateDMVisibleAside
} = footprintSlice.actions; } = footprintSlice.actions;
export default footprintSlice.reducer; export default footprintSlice.reducer;
+36 -33
View File
@@ -1,11 +1,12 @@
import AgoraRTC, { ICameraVideoTrack, IMicrophoneAudioTrack } from 'agora-rtc-sdk-ng'; import AgoraRTC, { ICameraVideoTrack, IMicrophoneAudioTrack } from 'agora-rtc-sdk-ng';
import { memo, useEffect } from 'react'; import { memo, useEffect } from 'react';
import { useDispatch } from 'react-redux'; 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 { updateChannelVisibleAside } from '../../app/slices/footprint';
import { addVoiceMember, removeVoiceMember, updateConnectionState, updateDeafenStatus, updateMuteStatus, updateVoicingInfo, updateVoicingMember, updateVoicingNetworkQuality, upsertVoiceList } from '../../app/slices/voice'; import { addVoiceMember, removeVoiceMember, updateConnectionState, updateDeafenStatus, updateMuteStatus, updateVoicingInfo, updateVoicingMember, updateVoicingNetworkQuality, upsertVoiceList } from '../../app/slices/voice';
import { useAppSelector } from '../../app/store'; import { useAppSelector } from '../../app/store';
import AudioJoin from '../../assets/join.wav'; import AudioJoin from '../../assets/join.wav';
import { playAgoraVideo } from '../utils';
// import { compareVersion } from '../utils'; // import { compareVersion } from '../utils';
// type Props = {} // type Props = {}
AgoraRTC.setLogLevel(process.env.NODE_ENV === 'development' ? 0 : 4); AgoraRTC.setLogLevel(process.env.NODE_ENV === 'development' ? 0 : 4);
@@ -42,12 +43,8 @@ const Voice = () => {
window.VOICE_TRACK_MAP[+user.uid] = user.audioTrack; window.VOICE_TRACK_MAP[+user.uid] = user.audioTrack;
} }
if (mediaType == "video") { 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; window.VIDEO_TRACK_MAP[+user.uid] = user.videoTrack;
playAgoraVideo(+user.uid);
} }
agoraEngine.on("user-unpublished", (user) => { agoraEngine.on("user-unpublished", (user) => {
if (!user.hasAudio) { if (!user.hasAudio) {
@@ -112,14 +109,14 @@ const Voice = () => {
}); });
// 有新用户加入 // 有新用户加入
agoraEngine.on("user-joined", async (user) => { 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)); dispatch(addVoiceMember(+user.uid));
}); });
window.VOICE_CLIENT = agoraEngine; window.VOICE_CLIENT = agoraEngine;
}; };
const handlePageUnload = (evt: BeforeUnloadEvent) => { const handlePageUnload = (evt: BeforeUnloadEvent) => {
console.log("unload"); console.log("unload");
if (localAudioTrack) { if (window.VOICE_CLIENT?.connectionState === "CONNECTED") {
evt.preventDefault(); evt.preventDefault();
return (evt.returnValue = ""); return (evt.returnValue = "");
} }
@@ -131,14 +128,15 @@ const Voice = () => {
return () => { return () => {
window.removeEventListener("beforeunload", handlePageUnload, { capture: true }); window.removeEventListener("beforeunload", handlePageUnload, { capture: true });
}; };
}, []); }, []);
return null; return null;
}; };
let localAudioTrack: IMicrophoneAudioTrack | null = null; // let localAudioTrack: IMicrophoneAudioTrack | null = null;
let localVideoTrack: ICameraVideoTrack | null = null; // let localVideoTrack: ICameraVideoTrack | null = null;
type VoiceProps = { type VoiceProps = {
id: number, id: number,
context?: "channel" | "dm" context?: "channel" | "dm"
@@ -148,11 +146,11 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { voicingInfo, loginUid } = useAppSelector(store => { const { voicingInfo, loginUid } = useAppSelector(store => {
return { return {
loginUid: store.authData.user?.uid, loginUid: store.authData.user?.uid ?? 0,
voicingInfo: store.voice.voicing voicingInfo: store.voice.voicing
}; };
}); });
const [generateToken] = useLazyGetAgoraTokenQuery(); const [generateToken] = useGenerateAgoraTokenMutation();
// const [joining, setJoining] = useState(false); // const [joining, setJoining] = useState(false);
const joinVoice = async () => { const joinVoice = async () => {
// setJoining(true); // setJoining(true);
@@ -161,14 +159,21 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
context, context,
joining: true joining: true
})); }));
const { isError, data } = await generateToken(id); const resp = await generateToken(context == "channel" ? { gid: id } : { uid: id });
if (!isError && data) { if ('error' in resp) {
const { channel_name, app_id, agora_token, uid } = data; 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) { if (window.VOICE_CLIENT) {
await window.VOICE_CLIENT.join(app_id, channel_name, agora_token, uid); await window.VOICE_CLIENT.join(app_id, channel_name, agora_token, uid);
console.table(data);
// Create a local audio track from the microphone audio. // 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. // Publish the local audio track in the channel.
await window.VOICE_CLIENT.publish(localAudioTrack); await window.VOICE_CLIENT.publish(localAudioTrack);
// play the join audio // play the join audio
@@ -183,37 +188,31 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
})); }));
// 把自己加进去 // 把自己加进去
dispatch(addVoiceMember(uid)); dispatch(addVoiceMember(uid));
// 放到全局变量里
window.VOICE_TRACK_MAP[loginUid] = localAudioTrack;
} }
} else {
console.error("generate agora token error");
dispatch(updateVoicingInfo({
joining: false,
id,
context,
}));
} }
// setJoining(false); // setJoining(false);
}; };
const openCamera = async () => { const openCamera = async () => {
localVideoTrack = await AgoraRTC.createCameraVideoTrack(); const localVideoTrack = await AgoraRTC.createCameraVideoTrack();
await window.VOICE_CLIENT?.publish(localVideoTrack); 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({ dispatch(updateVoicingInfo({
video: true, video: true,
id, id,
context, context,
})); }));
// 放到全局变量里
window.VIDEO_TRACK_MAP[loginUid] = localVideoTrack;
playAgoraVideo(loginUid);
}; };
const closeCamera = async () => { const closeCamera = async () => {
const localVideoTrack = window.VIDEO_TRACK_MAP[loginUid] as ICameraVideoTrack;
if (localVideoTrack) { if (localVideoTrack) {
await window.VOICE_CLIENT?.unpublish(localVideoTrack); await window.VOICE_CLIENT?.unpublish(localVideoTrack);
localVideoTrack.close(); localVideoTrack.close();
localVideoTrack = null; window.VIDEO_TRACK_MAP[loginUid] = null;
// 关闭视频后,需要把视频的高度设置回去 // 关闭视频后,需要把视频的高度设置回去
const playerEle = document.querySelector(`#CAMERA_${loginUid}`) as HTMLElement; const playerEle = document.querySelector(`#CAMERA_${loginUid}`) as HTMLElement;
playerEle.classList.remove("h-[120px]"); playerEle.classList.remove("h-[120px]");
@@ -226,12 +225,14 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
}; };
const leave = async () => { 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) { if (window.VOICE_CLIENT && localAudioTrack) {
localAudioTrack.close(); localAudioTrack.close();
localAudioTrack = null; window.VOICE_TRACK_MAP[loginUid] = null;
if (localVideoTrack) { if (localVideoTrack) {
localVideoTrack.close(); localVideoTrack.close();
localVideoTrack = null; window.VIDEO_TRACK_MAP[loginUid] = null;
} }
await window.VOICE_CLIENT.leave(); await window.VOICE_CLIENT.leave();
dispatch(updateVoicingInfo(null)); dispatch(updateVoicingInfo(null));
@@ -249,12 +250,14 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
} }
}; };
const setMute = (mute: boolean) => { const setMute = (mute: boolean) => {
const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack;
if (localAudioTrack) { if (localAudioTrack) {
localAudioTrack.setMuted(mute); localAudioTrack.setMuted(mute);
dispatch(updateMuteStatus(mute)); dispatch(updateMuteStatus(mute));
} }
}; };
const setDeafen = (deafen: boolean) => { const setDeafen = (deafen: boolean) => {
const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack;
if (localAudioTrack) { if (localAudioTrack) {
if (deafen) { if (deafen) {
localAudioTrack.setMuted(true); localAudioTrack.setMuted(true);
+13
View File
@@ -1,5 +1,6 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import BASE_URL, { FILE_IMAGE_SIZE, ContentTypes, KEY_TOKEN, KEY_REFRESH_TOKEN, KEY_EXPIRE } from "../app/config"; 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 IconPdf from "../assets/icons/file.pdf.svg";
import IconAudio from "../assets/icons/file.audio.svg"; import IconAudio from "../assets/icons/file.audio.svg";
import IconVideo from "../assets/icons/file.video.svg"; import IconVideo from "../assets/icons/file.video.svg";
@@ -371,4 +372,16 @@ export const fromNowTime = (ts?: number) => {
if (!ts) return null; if (!ts) return null;
const currTS = + new Date(); const currTS = + new Date();
return dayjs(ts > currTS ? currTS : ts).fromNow(); 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);
}
}
}; };
+5
View File
@@ -8,6 +8,8 @@ import User from "../../../common/component/User";
import Layout from "../Layout"; import Layout from "../Layout";
import { useAppSelector } from "../../../app/store"; import { useAppSelector } from "../../../app/store";
import GoBackNav from "../../../common/component/GoBackNav"; import GoBackNav from "../../../common/component/GoBackNav";
import ServerVersionChecker from "../../../common/component/ServerVersionChecker";
import VoiceChat from "../VoiceChat";
type Props = { type Props = {
uid: number; uid: number;
dropFiles?: File[]; dropFiles?: File[];
@@ -33,6 +35,9 @@ const DMChat: FC<Props> = ({ uid = 0, dropFiles }) => {
dropFiles={dropFiles} dropFiles={dropFiles}
aside={ aside={
<ul className="flex flex-col gap-6"> <ul className="flex flex-col gap-6">
<ServerVersionChecker version="0.3.6" empty={true}>
<VoiceChat context={`dm`} id={uid} />
</ServerVersionChecker>
<Tooltip tip="Saved Items" placement="left"> <Tooltip tip="Saved Items" placement="left">
<Tippy <Tippy
placement="left-start" placement="left-start"
@@ -1,5 +1,5 @@
import clsx from 'clsx'; import clsx from 'clsx';
// import React from 'react'; import { useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { VoicingInfo } from '../../../app/slices/voice'; import { VoicingInfo } from '../../../app/slices/voice';
@@ -15,6 +15,7 @@ import IconScreen from '../../../assets/icons/share.screen.svg';
import StyledButton from '../../../common/component/styled/Button'; import StyledButton from '../../../common/component/styled/Button';
import IconCallOff from '../../../assets/icons/call.off.svg'; import IconCallOff from '../../../assets/icons/call.off.svg';
import Tooltip from '../../../common/component/Tooltip'; import Tooltip from '../../../common/component/Tooltip';
import { playAgoraVideo } from '../../../common/utils';
// import User from '../../../common/component/User'; // import User from '../../../common/component/User';
type Props = { type Props = {
@@ -34,6 +35,13 @@ const VoiceManagement = ({ info, setMute, setDeafen, leave, closeCamera, openCam
voicingMembers: store.voice.voicingMembers voicingMembers: store.voice.voicingMembers
}; };
}); });
useEffect(() => {
const ids = voicingMembers.ids;
ids.forEach(id => {
playAgoraVideo(id);
});
}, [voicingMembers.ids]);
if (!info) return null; if (!info) return null;
const { deafen, muted, video } = info; const { deafen, muted, video } = info;
const nameClass = clsx(`text-sm text-gray-500 max-w-[120px] truncate font-semibold dark:text-white`); const nameClass = clsx(`text-sm text-gray-500 max-w-[120px] truncate font-semibold dark:text-white`);
+11 -14
View File
@@ -3,7 +3,7 @@
// import { useState } from 'react'; // import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { updateChannelVisibleAside } from '../../../app/slices/footprint'; import { updateChannelVisibleAside, updateDMVisibleAside } from '../../../app/slices/footprint';
import { useAppSelector } from '../../../app/store'; import { useAppSelector } from '../../../app/store';
import IconHeadphone from '../../../assets/icons/headphone.svg'; import IconHeadphone from '../../../assets/icons/headphone.svg';
import Tooltip from '../../../common/component/Tooltip'; import Tooltip from '../../../common/component/Tooltip';
@@ -29,13 +29,11 @@ const VoiceChat = ({ id, context = "channel" }: Props) => {
const { data: enabled } = useGetAgoraStatusQuery(); const { data: enabled } = useGetAgoraStatusQuery();
const { t } = useTranslation("chat"); const { t } = useTranslation("chat");
const toggleDashboard = () => { const toggleDashboard = () => {
if (context == "channel") { const data = {
dispatch(updateChannelVisibleAside({ id,
id, aside: visibleAside == "voice" ? null : "voice" as const
aside: visibleAside == "voice" ? null : "voice" };
})); dispatch(context == "channel" ? updateChannelVisibleAside(data) : updateDMVisibleAside(data));
}
// todo DM
}; };
const handleJoin = () => { const handleJoin = () => {
if (joining || joined) { if (joining || joined) {
@@ -43,12 +41,11 @@ const VoiceChat = ({ id, context = "channel" }: Props) => {
return; return;
} }
joinVoice(); joinVoice();
if (context == "channel") { const data = {
dispatch(updateChannelVisibleAside({ id,
id, aside: "voice" as const
aside: "voice" };
})); dispatch(context == "channel" ? updateChannelVisibleAside(data) : updateDMVisibleAside(data));
}
}; };
if (!loginUser || !enabled) return null; if (!loginUser || !enabled) return null;
const visible = visibleAside == "voice"; const visible = visibleAside == "voice";
+1 -1
View File
@@ -50,7 +50,7 @@ export default function ChannelSetting() {
pathPrefix={`/setting/channel/${cid}`} pathPrefix={`/setting/channel/${cid}`}
nav={currNav} nav={currNav}
closeModal={close} closeModal={close}
title="Channel Setting" title="Channel Settings"
navs={navs} navs={navs}
dangers={[ dangers={[
canLeave && { canLeave && {