feat: DM voice (draft)
This commit is contained in:
@@ -19,7 +19,8 @@ import {
|
||||
RenewLicenseResponse,
|
||||
AgoraTokenResponse,
|
||||
AgoraVoicingListResponse,
|
||||
SystemCommon
|
||||
SystemCommon,
|
||||
AgoraChannelUsersResponse
|
||||
} from "../../types/server";
|
||||
import { Channel } from "../../types/channel";
|
||||
import { ContentTypeKey } from "../../types/message";
|
||||
@@ -130,7 +131,8 @@ export const serverApi = createApi({
|
||||
return {
|
||||
id: +id,
|
||||
context,
|
||||
memberCount: count
|
||||
memberCount: count,
|
||||
channelName: data.channel_name
|
||||
};
|
||||
});
|
||||
dispatch(upsertVoiceList(arr));
|
||||
@@ -140,6 +142,15 @@ export const serverApi = createApi({
|
||||
}
|
||||
}
|
||||
}),
|
||||
getAgoraUsersByChannel: builder.query<number[], string>({
|
||||
query: (channel_name) => ({ url: `/admin/agora/channel/user/${channel_name}/false` }),
|
||||
transformResponse: (resp: AgoraChannelUsersResponse) => {
|
||||
if (resp.success && resp.data.channel_exist) {
|
||||
return resp.data.users ?? [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
updateAgoraConfig: builder.mutation<void, AgoraConfig>({
|
||||
query: (data) => ({
|
||||
url: `/admin/agora/config`,
|
||||
@@ -405,5 +416,6 @@ export const {
|
||||
useUpdateSystemCommonMutation,
|
||||
useLazyGetSystemCommonQuery,
|
||||
useGetSystemCommonQuery,
|
||||
useGenerateAgoraTokenMutation
|
||||
useGenerateAgoraTokenMutation,
|
||||
useLazyGetAgoraUsersByChannelQuery
|
||||
} = serverApi;
|
||||
|
||||
+20
-5
@@ -4,8 +4,9 @@ import { ConnectionState } from "agora-rtc-sdk-ng";
|
||||
import { resetAuthData } from "./auth.data";
|
||||
|
||||
export type VoiceBasicInfo = {
|
||||
context: "channel" | "dm"
|
||||
id: number,
|
||||
context: "channel" | "dm",
|
||||
from?: number,
|
||||
id: number,// means to in dm context
|
||||
}
|
||||
|
||||
export type VoicingInfo = {
|
||||
@@ -34,9 +35,12 @@ export type VoicingMembers = {
|
||||
pin?: number
|
||||
}
|
||||
export type VoiceInfo = {
|
||||
channelName: string,
|
||||
memberCount: number
|
||||
} & VoiceBasicInfo
|
||||
interface State {
|
||||
callingFrom: number,
|
||||
callingTo: number,
|
||||
voicing: VoicingInfo | null,
|
||||
voicingMembers: VoicingMembers,
|
||||
list: VoiceInfo[]
|
||||
@@ -47,6 +51,8 @@ interface State {
|
||||
// members: []
|
||||
// };
|
||||
const initialState: State = {
|
||||
callingFrom: 0,
|
||||
callingTo: 0,
|
||||
voicing: null,
|
||||
voicingMembers: {
|
||||
ids: [],
|
||||
@@ -61,6 +67,11 @@ const voiceSlice = createSlice({
|
||||
name: "voice",
|
||||
initialState,
|
||||
reducers: {
|
||||
updateCalling(state, { payload }: PayloadAction<{ from: number, to?: number }>) {
|
||||
const { from, to = 0 } = payload;
|
||||
state.callingFrom = from;
|
||||
state.callingTo = to;
|
||||
},
|
||||
updateVoicingInfo(state, { payload }: PayloadAction<VoicingInfo | null>) {
|
||||
if (payload) {
|
||||
state.voicing = { ...(state.voicing ?? {}), ...payload };
|
||||
@@ -157,11 +168,15 @@ const voiceSlice = createSlice({
|
||||
if (window.VOICE_CLIENT) {
|
||||
window.VOICE_CLIENT.leave();
|
||||
Object.entries(window.VOICE_TRACK_MAP).forEach(([uid, track]) => {
|
||||
track?.close();
|
||||
if (track && 'close' in track) {
|
||||
track.close();
|
||||
}
|
||||
delete window.VOICE_TRACK_MAP[+uid];
|
||||
});
|
||||
Object.entries(window.VIDEO_TRACK_MAP).forEach(([uid, track]) => {
|
||||
track?.close();
|
||||
if (track && 'close' in track) {
|
||||
track?.close();
|
||||
}
|
||||
delete window.VOICE_TRACK_MAP[+uid];
|
||||
});
|
||||
}
|
||||
@@ -174,5 +189,5 @@ const voiceSlice = createSlice({
|
||||
}
|
||||
});
|
||||
|
||||
export const { updatePin, updateConnectionState, addVoiceMember, removeVoiceMember, upsertVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateMuteStatus, updateVoicingMember, updateDeafenStatus } = voiceSlice.actions;
|
||||
export const { updateCalling, updatePin, updateConnectionState, addVoiceMember, removeVoiceMember, upsertVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateMuteStatus, updateVoicingMember, updateDeafenStatus } = voiceSlice.actions;
|
||||
export default voiceSlice.reducer;
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// import React from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Waveform } from '@uiball/loaders';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useAppSelector } from '../../../app/store';
|
||||
import IconCallOff from '../../../assets/icons/call.off.svg';
|
||||
import IconCallAnswer from '../../../assets/icons/call.svg';
|
||||
import IconMicOff from '../../../assets/icons/mic.off.svg';
|
||||
import IconMic from '../../../assets/icons/mic.on.svg';
|
||||
import IconCamera from '../../../assets/icons/camera.svg';
|
||||
import IconCameraOff from '../../../assets/icons/camera.off.svg';
|
||||
import IconScreen from '../../../assets/icons/share.screen.svg';
|
||||
import Avatar from '../Avatar';
|
||||
import { VoicingInfo, updateCalling } from '../../../app/slices/voice';
|
||||
import useVoice from './useVoice';
|
||||
import { playAgoraVideo } from '../../utils';
|
||||
import Tooltip from '../Tooltip';
|
||||
|
||||
type Props = {
|
||||
from: number
|
||||
to?: number
|
||||
}
|
||||
|
||||
const DMCalling = ({ from, to = 0 }: Props) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const { leave, joinVoice, joining, setMute, closeCamera, openCamera, startShareScreen, stopShareScreen } = useVoice({ id: to, context: "dm" });
|
||||
const containerRef = useRef(null);
|
||||
const dispatch = useDispatch();
|
||||
const { voicingInfo, voicingMembers, fromUser, toUser, loginUser } = useAppSelector(store => {
|
||||
return {
|
||||
voicingInfo: store.voice.voicing ?? {},
|
||||
voicingMembers: store.voice.voicingMembers,
|
||||
fromUser: store.users.byId[from],
|
||||
toUser: store.users.byId[to],
|
||||
loginUser: store.authData.user
|
||||
};
|
||||
});
|
||||
const sendByMe = loginUser?.uid !== toUser.uid;
|
||||
const handleCancel = () => {
|
||||
console.log('cancel');
|
||||
if (sendByMe || voicingMembers.ids.length == 2) {
|
||||
leave();
|
||||
}
|
||||
dispatch(updateCalling({ from: 0, to: 0 }));
|
||||
};
|
||||
const handleAnswer = () => {
|
||||
joinVoice();
|
||||
};
|
||||
useEffect(() => {
|
||||
const ids = voicingMembers.ids;
|
||||
ids.forEach(id => {
|
||||
playAgoraVideo(id);
|
||||
});
|
||||
}, [voicingMembers.ids]);
|
||||
if (!fromUser || !toUser) return null;
|
||||
const connected = voicingMembers.ids.length == 2;
|
||||
const { name, avatar } = sendByMe ? toUser : fromUser;
|
||||
const { muted, shareScreen, video } = voicingInfo as VoicingInfo;
|
||||
const { speakingVolume = 0 } = voicingMembers.byId[sendByMe ? to : from] ?? {};
|
||||
const speaking = speakingVolume > 50;
|
||||
return (
|
||||
<div ref={containerRef} className="fixed top-0 left-0 w-full h-screen z-[999] pointer-events-none flex items-center justify-end pr-10">
|
||||
<motion.aside drag dragConstraints={containerRef} dragMomentum={false} whileDrag={{ scale: 1.1 }} className={clsx(`pointer-events-auto
|
||||
rounded bg-gray-800 relative
|
||||
shadow-lg shadow-slate-200 dark:shadow-slate-800
|
||||
cursor-move overflow-hidden
|
||||
`, connected ? "w-80 h-96" : "w-64 h-80")}>
|
||||
<div className={clsx("w-20 h-20 flex shrink-0 relative transition-opacity mt-32 m-auto")}>
|
||||
{speaking && <div className={clsx("absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 rounded-full bg-green-500 animate-speaking", "w-[86px] h-[86px]")}></div>}
|
||||
<Avatar
|
||||
width={80}
|
||||
height={80}
|
||||
className="w-full h-full rounded-full object-cover"
|
||||
src={avatar}
|
||||
name={name}
|
||||
alt="avatar"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute left-0 top-0 w-full h-full" id={`CAMERA_${from}`}>
|
||||
{/* from camera video */}
|
||||
</div>
|
||||
<div className="absolute right-0-0 top-0 w-40 h-32" id={`CAMERA_${to}`}>
|
||||
{/* to camera video */}
|
||||
</div>
|
||||
<div className={clsx("absolute left-0 top-0 py-5 w-full h-full flex flex-col justify-between items-center", connected ? "bg-transparent" : "bg-gray-800")}>
|
||||
{!connected && <>
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
<div className="rounded-full overflow-hidden w-20 h-20 shrink-0">
|
||||
<Avatar name={name} src={avatar} width={80} height={80} className='h-20' />
|
||||
</div>
|
||||
<span className='text-white mb-2'>{name}</span>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1 items-center my-4'>
|
||||
<Waveform
|
||||
size={18}
|
||||
lineWeight={3}
|
||||
speed={1}
|
||||
color='#aaa'
|
||||
/>
|
||||
<span className='text-xs text-gray-600 dark:text-gray-400'>{sendByMe ? `Calling` : `Incoming call`}</span>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
<div className={clsx("flex gap-3", connected ? "h-full items-end" : "")}>
|
||||
<Tooltip tip={"Disconnect"} placement="top">
|
||||
<button onClick={handleCancel} className='flex-center bg-red-600 hover:bg-red-700 py-2 px-3 rounded-lg'>
|
||||
<IconCallOff className="w-6 h-6" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{!sendByMe && !connected &&
|
||||
<Tooltip tip={"Answer"} placement="top">
|
||||
<button disabled={joining} onClick={handleAnswer} className='flex-center bg-green-600 hover:bg-green-700 py-2 px-3 rounded-lg'>
|
||||
<IconCallAnswer className="w-6 h-6 fill-white" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
}
|
||||
{connected && <>
|
||||
<Tooltip tip={muted ? t("unmute") : t("mute")} placement="top">
|
||||
<button onClick={setMute.bind(null, !muted)} className="flex-center py-2 px-3 rounded-lg bg-gray-100 dark:bg-gray-900">
|
||||
{muted ? <IconMicOff className="fill-gray-700 dark:fill-gray-200" /> : <IconMic className="fill-gray-700 dark:fill-gray-200" />}
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip tip={video ? t("camera_off") : t("camera_on")} placement="top">
|
||||
<button onClick={video ? closeCamera : openCamera} className="flex-center py-2 px-3 rounded-lg bg-gray-100 dark:bg-gray-900">
|
||||
{video ? <IconCamera className="fill-gray-700 dark:fill-gray-200" /> : <IconCameraOff className="fill-gray-700 dark:fill-gray-200" />}
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip tip={"Share Screen"} placement="top">
|
||||
<button onClick={shareScreen ? stopShareScreen : startShareScreen} className={clsx("flex-center py-2 px-3 rounded-lg ", shareScreen ? "bg-green-700" : "bg-gray-100 dark:bg-gray-900")}>
|
||||
<IconScreen className={clsx("dark:fill-gray-200 w-6 h-6", shareScreen ? "fill-gray-200" : "fill-gray-700")} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
</>}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</motion.aside>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DMCalling;
|
||||
@@ -0,0 +1,173 @@
|
||||
import AgoraRTC from 'agora-rtc-sdk-ng';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useGetAgoraChannelsQuery, useGetAgoraStatusQuery, useLazyGetAgoraUsersByChannelQuery } from '../../../app/services/server';
|
||||
import { addVoiceMember, removeVoiceMember, updateCalling, updateConnectionState, updateVoicingMember, updateVoicingNetworkQuality } from '../../../app/slices/voice';
|
||||
import { useAppSelector } from '../../../app/store';
|
||||
import { playAgoraVideo } from '../../utils';
|
||||
import useVoice from './useVoice';
|
||||
import DMCalling from './DMCalling';
|
||||
|
||||
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 { from, to, voiceList, loginUid } = useAppSelector(store => {
|
||||
return {
|
||||
voiceList: store.voice.list,
|
||||
from: store.voice.callingFrom,
|
||||
to: store.voice.callingTo,
|
||||
loginUid: store.authData.user?.uid
|
||||
};
|
||||
});
|
||||
const { data: enabled } = useGetAgoraStatusQuery();
|
||||
const [getUsersByChannel] = useLazyGetAgoraUsersByChannelQuery();
|
||||
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", reason);
|
||||
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 });
|
||||
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
// 有人呼叫我
|
||||
const callMeList = voiceList.filter(item => item.context == "dm" && item.id == loginUid);
|
||||
if (callMeList.length) {
|
||||
const [firstCall] = callMeList;
|
||||
const { memberCount, channelName, id } = firstCall;
|
||||
if (memberCount == 1) {
|
||||
getUsersByChannel(channelName).then(resp => {
|
||||
const [uid] = resp.data ?? [];
|
||||
if (uid) {
|
||||
dispatch(updateCalling({ from: uid, to: id }));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [voiceList, loginUid]);
|
||||
// return <DMCalling uid={1} sendByMe={calling !== loginUid} />;
|
||||
if (from !== 0) return <DMCalling from={from} to={to} />;
|
||||
return null;
|
||||
};
|
||||
|
||||
export { useVoice };
|
||||
export default memo(Voice);
|
||||
@@ -1,147 +1,16 @@
|
||||
import AgoraRTC, { ICameraVideoTrack, IMicrophoneAudioTrack } from 'agora-rtc-sdk-ng';
|
||||
import { memo, useEffect } from 'react';
|
||||
|
||||
import { ShareScreenTrack } from '../../../types/global';
|
||||
import AudioJoin from '../../../assets/join.wav';
|
||||
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, updatePin, 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);
|
||||
import { useGenerateAgoraTokenMutation } from '../../../app/services/server';
|
||||
import { updateChannelVisibleAside, updateDMVisibleAside } from '../../../app/slices/footprint';
|
||||
|
||||
if (label?.includes("screen")) {
|
||||
// 远端用户共享屏幕
|
||||
// const screenTrack = user.videoTrack as ShareScreenTrack;
|
||||
// screenTrack.on("track-ended", () => {
|
||||
// // 远端用户停止共享屏幕
|
||||
dispatch(updateVoicingMember({ uid: +user.uid, info: { shareScreen: true } }));
|
||||
}
|
||||
import { addVoiceMember, updateDeafenStatus, updateMuteStatus, updatePin, updateVoicingInfo, upsertVoiceList } from '../../../app/slices/voice';
|
||||
import { useAppSelector } from '../../../app/store';
|
||||
import { playAgoraVideo } from '../../utils';
|
||||
|
||||
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"
|
||||
@@ -305,17 +174,16 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
|
||||
localVideoTrack.close();
|
||||
window.VIDEO_TRACK_MAP[loginUid] = null;
|
||||
}
|
||||
if (context == "channel") {
|
||||
dispatch(updateChannelVisibleAside({
|
||||
id, aside: null
|
||||
}));
|
||||
// 即时更新对应的活跃列表信息
|
||||
dispatch(upsertVoiceList({
|
||||
id,
|
||||
context,
|
||||
memberCount: 0
|
||||
}));
|
||||
}
|
||||
const updateAside = context == "channel" ? updateChannelVisibleAside : updateDMVisibleAside;
|
||||
dispatch(updateAside({ id, aside: null }));
|
||||
// 即时更新对应的活跃列表信息
|
||||
dispatch(upsertVoiceList({
|
||||
id,
|
||||
context,
|
||||
memberCount: 0,
|
||||
// will fix it
|
||||
channelName: `vocechat:${context}:${id}`,
|
||||
}));
|
||||
}
|
||||
};
|
||||
const setMute = (mute: boolean) => {
|
||||
@@ -366,5 +234,5 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
|
||||
stopShareScreen,
|
||||
};
|
||||
};
|
||||
export { useVoice };
|
||||
export default memo(Voice);
|
||||
|
||||
export default useVoice;
|
||||
@@ -10,6 +10,7 @@ import Tooltip from '../../../common/component/Tooltip';
|
||||
import { useVoice } from '../../../common/component/Voice';
|
||||
import { useGetAgoraStatusQuery } from '../../../app/services/server';
|
||||
import { ChatContext } from '../../../types/common';
|
||||
import { updateCalling } from '../../../app/slices/voice';
|
||||
|
||||
type Props = {
|
||||
context?: ChatContext
|
||||
@@ -47,6 +48,10 @@ const VoiceChat = ({ id, context = "channel" }: Props) => {
|
||||
aside: "voice" as const
|
||||
};
|
||||
dispatch(context == "channel" ? updateChannelVisibleAside(data) : updateDMVisibleAside(data));
|
||||
// 实时显示calling box
|
||||
if (!joinedAtThisContext && context == "dm") {
|
||||
dispatch(updateCalling({ from: loginUser?.uid ?? 0, to: id }));
|
||||
}
|
||||
};
|
||||
if (!loginUser || !enabled) return null;
|
||||
const visible = visibleAside == "voice";
|
||||
|
||||
@@ -38,6 +38,15 @@ export interface AgoraVoicingListResponse {
|
||||
total_size: number
|
||||
}
|
||||
}
|
||||
export interface AgoraChannelUsersResponse {
|
||||
success: boolean,
|
||||
data: {
|
||||
channel_exist: boolean,
|
||||
mode?: number,
|
||||
total?: number,
|
||||
users?: number[]
|
||||
}
|
||||
}
|
||||
export interface AgoraTokenResponse {
|
||||
agora_token: string,
|
||||
uid: number,
|
||||
|
||||
Reference in New Issue
Block a user