diff --git a/src/app/slices/voice.ts b/src/app/slices/voice.ts index b0acf632..6ae94b4a 100644 --- a/src/app/slices/voice.ts +++ b/src/app/slices/voice.ts @@ -5,6 +5,8 @@ export type VoiceBasicInfo = { id: number, } export type VoicingInfo = { + downlinkNetworkQuality?: number, + muted: boolean, members: number[] } & VoiceBasicInfo export type VoiceInfo = { @@ -31,8 +33,25 @@ const voiceSlice = createSlice({ initialState, reducers: { updateVoicingInfo(state, { payload }: PayloadAction) { + if (payload && state.voicing) { + const { members, ...rest } = payload; + const tmpArr = [...state.voicing.members, ...members]; + console.log("tmmp arr", tmpArr); + + state.voicing = { ...rest, members: Array.from(new Set(tmpArr)) }; + } state.voicing = payload; }, + updateMuteStatus(state, { payload }: PayloadAction) { + if (state.voicing) { + state.voicing.muted = payload; + } + }, + updateVoicingNetworkQuality(state, { payload }: PayloadAction) { + if (state.voicing) { + state.voicing.downlinkNetworkQuality = payload; + } + }, updateVoiceList(state, { payload }: PayloadAction) { state.list = payload; }, @@ -41,6 +60,14 @@ const voiceSlice = createSlice({ if (!state.voicing.members.includes(payload)) { state.voicing.members.push(payload); } + } else { + // tricky? + state.voicing = { + id: 0, + context: "channel", + muted: false, + members: [payload] + }; } }, removeVoiceMember(state, { payload }: PayloadAction) { @@ -54,5 +81,5 @@ const voiceSlice = createSlice({ }, }); -export const { addVoiceMember, removeVoiceMember, updateVoiceList, updateVoicingInfo } = voiceSlice.actions; +export const { addVoiceMember, removeVoiceMember, updateVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateMuteStatus } = voiceSlice.actions; export default voiceSlice.reducer; diff --git a/src/assets/icons/mic.off.svg b/src/assets/icons/mic.off.svg new file mode 100644 index 00000000..398b25cd --- /dev/null +++ b/src/assets/icons/mic.off.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/mic.on.svg b/src/assets/icons/mic.on.svg index f39cdf89..e9be3642 100644 --- a/src/assets/icons/mic.on.svg +++ b/src/assets/icons/mic.on.svg @@ -1,4 +1,3 @@ - - - + + diff --git a/src/common/component/Signal.tsx b/src/common/component/Signal.tsx new file mode 100644 index 00000000..88679b52 --- /dev/null +++ b/src/common/component/Signal.tsx @@ -0,0 +1,32 @@ +import clsx from 'clsx'; +import React from 'react'; + +type Props = { + strength?: number +} +// 0:质量未知。 +// 1:质量极好。 +// 2:用户主观感觉和极好差不多,但码率可能略低于极好。 +// 3:用户主观感受有瑕疵但不影响沟通。 +// 4:勉强能沟通但不顺畅。 +// 5:网络质量非常差,基本不能沟通。 +// 6: 网络连接断开,完全无法沟通。 + +const Signal = ({ strength = 0 }: Props) => { + const finalStrength = 6 - strength; + const color = finalStrength <= 2 ? `bg-red-700` : (finalStrength == 3 ? `bg-yellow-700` : `bg-green-700`); + let bgColor = ` bg-gray-500`; + return ( +
+
+ + + + + +
+
+ ); +}; + +export default Signal; \ No newline at end of file diff --git a/src/common/component/Voice.tsx b/src/common/component/Voice.tsx index ead576f6..d3f41048 100644 --- a/src/common/component/Voice.tsx +++ b/src/common/component/Voice.tsx @@ -2,7 +2,7 @@ import AgoraRTC, { IMicrophoneAudioTrack } from 'agora-rtc-sdk-ng'; import { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useGetAgoraConfigQuery, useGetAgoraVoicingListQuery, useLazyGetAgoraTokenQuery } from '../../app/services/server'; -import { addVoiceMember, removeVoiceMember, updateVoicingInfo } from '../../app/slices/voice'; +import { addVoiceMember, removeVoiceMember, updateMuteStatus, updateVoicingInfo, updateVoicingNetworkQuality } from '../../app/slices/voice'; import { useAppSelector } from '../../app/store'; // type Props = {} @@ -31,22 +31,45 @@ const Voice = () => { 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); - dispatch(addVoiceMember(+user.uid)); - // console.log("subscribe success"); // Subscribe and play the remote audio track. - console.log(user.uid + "has joined the channel"); + console.log(user, " has published at the channel"); if (mediaType == "audio") { // Play the remote audio track. user.audioTrack?.play(); window.VOICE_TRACK_MAP[+user.uid] = user.audioTrack; - // console.log("Remote user connected: " + user.uid); } // Listen for the "user-unpublished" event. agoraEngine.on("user-unpublished", user => { dispatch(removeVoiceMember(+user.uid as number)); console.log(user.uid + "has left the channel"); - // console.log("Remote user has left the channel"); }); + // 信号强度 + agoraEngine.on("network-quality", (qlt) => { + const { downlinkNetworkQuality } = qlt; + dispatch(updateVoicingNetworkQuality(downlinkNetworkQuality)); + }); + // 用户状态变化 + agoraEngine.on("user-info-updated", (uid, msg) => { + console.log("user-info-updated", uid, msg); + switch (msg) { + case "mute-audio": + // todo + break; + case "unmute-audio": + // todo + break; + + default: + break; + } + // const { downlinkNetworkQuality } = qlt; + // dispatch(updateVoicingNetworkQuality(downlinkNetworkQuality)); + }); + }); + // 有新用户加入 + agoraEngine.on("user-joined", async (user) => { + console.log(user, " has joined the channel"); + dispatch(addVoiceMember(+user.uid)); }); window.VOICE_CLIENT = agoraEngine; }; @@ -56,7 +79,8 @@ const Voice = () => { } return () => { - if (window.VOICE_CLIENT) { + if (window.VOICE_CLIENT && localTrack) { + localTrack.close(); window.VOICE_CLIENT.leave(); } // window.VOICE_CLIENT=null @@ -66,17 +90,17 @@ const Voice = () => { return null; }; +let localTrack: IMicrophoneAudioTrack | null = null; type VoiceProps = { id: number, context?: "channel" | "dm" } const useVoice = ({ id, context = "channel" }: VoiceProps) => { const dispatch = useDispatch(); - const { voiceInfo, joined, loginUid = 0 } = useAppSelector(store => { + const { voicingInfo } = useAppSelector(store => { return { - loginUid: store.authData.user?.uid, - voiceInfo: store.voice.voicing, - joined: !!store.voice.voicing + // loginUid: store.authData.user?.uid, + voicingInfo: store.voice.voicing }; }); const [generateToken] = useLazyGetAgoraTokenQuery(); @@ -90,12 +114,13 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { await window.VOICE_CLIENT.join(app_id, channel_name, agora_token, uid); console.table(data); // Create a local audio track from the microphone audio. - - window.VOICE_TRACK_MAP[loginUid] = await AgoraRTC.createMicrophoneAudioTrack(); + localTrack = await AgoraRTC.createMicrophoneAudioTrack(); // Publish the local audio track in the channel. - await window.VOICE_CLIENT.publish(window.VOICE_TRACK_MAP[loginUid]); - console.log("Publish success!"); + await window.VOICE_CLIENT.publish(localTrack); + console.log("Publish success!,joined the channel"); + dispatch(updateVoicingInfo({ + muted: false, id, context, members: [uid] @@ -105,20 +130,25 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { setJoining(false); }; const leave = async () => { - if (window.VOICE_CLIENT) { - (window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack).close(); + if (window.VOICE_CLIENT && localTrack) { + localTrack.close(); await window.VOICE_CLIENT.leave(); dispatch(updateVoicingInfo(null)); - // window.VOICE_TRACK_MAP[loginUid]?.stop(); - // window.VOICE_TRACK_MAP[loginUid] = null; + } + }; + const setMute = (mute: boolean) => { + if (localTrack) { + localTrack.setMuted(mute); + dispatch(updateMuteStatus(mute)); } }; return { + setMute, leave, // canVoice, - voiceInfo, + voicingInfo, joining, - joined, + joined: !!voicingInfo, joinVoice }; }; diff --git a/src/routes/chat/RTCWidget.tsx b/src/routes/chat/RTCWidget.tsx index cf966cd1..a72a3a15 100644 --- a/src/routes/chat/RTCWidget.tsx +++ b/src/routes/chat/RTCWidget.tsx @@ -3,9 +3,11 @@ import { useAppSelector } from '../../app/store'; import User from '../../common/component/User'; import IconHeadphone from '../../assets/icons/headphone.svg'; import IconMic from '../../assets/icons/mic.on.svg'; -import IconSoundOn from '../../assets/icons/sound.on.svg'; -import IconSignal from '../../assets/icons/signal.svg'; +import IconMicOff from '../../assets/icons/mic.off.svg'; +// import IconSoundOn from '../../assets/icons/sound.on.svg'; +// import IconSignal from '../../assets/icons/signal.svg'; import { useVoice } from '../../common/component/Voice'; +import Signal from '../../common/component/Signal'; type Props = { id: number, @@ -13,30 +15,31 @@ type Props = { } const RTCWidget = ({ id, context = "channel" }: Props) => { - const { leave } = useVoice({ context, id }); - const { loginUser, voicingInfo, channelData, userData } = useAppSelector(store => { + const { leave, voicingInfo, setMute } = useVoice({ context, id }); + const { loginUser, channelData, userData } = useAppSelector(store => { return { userData: store.users.byId, channelData: store.channels.byId, loginUser: store.authData.user, - voicingInfo: store.voice.voicing }; }); - if (!loginUser) return null; + if (!loginUser || !voicingInfo) return null; + const name = voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : userData[voicingInfo.id]?.name; + if (!name) return null; return (
- {voicingInfo && -
-
- -
- Voice Connected - {voicingInfo.context == "channel" ? "Channel" : "DM"} / {voicingInfo.context == "channel" ? channelData[voicingInfo.id].name : userData[voicingInfo.id].name} -
+ {/* {voicingInfo && */} +
+
+ +
+ Voice Connected + {voicingInfo.context == "channel" ? "Channel" : "DM"} / {voicingInfo.context == "channel" ? channelData[voicingInfo.id].name : userData[voicingInfo.id].name}
-
- } + +
+ {/* } */}
@@ -45,10 +48,15 @@ const RTCWidget = ({ id, context = "channel" }: Props) => { #{loginUser.uid}
-
- - + {/* {voicingInfo && */} +
+ {/* */} + {voicingInfo.muted ? + + : + }
+ {/* } */}
); diff --git a/src/routes/chat/VoiceChat/Dashboard.tsx b/src/routes/chat/VoiceChat/Dashboard.tsx index f4ef0f0e..41c20cd3 100644 --- a/src/routes/chat/VoiceChat/Dashboard.tsx +++ b/src/routes/chat/VoiceChat/Dashboard.tsx @@ -10,11 +10,13 @@ type Props = { } const Dashboard = ({ context = "channel", id }: Props) => { - const { joinVoice, joined, joining, voiceInfo } = useVoice({ id, context }); + const { joinVoice, joined, joining, voicingInfo } = useVoice({ id, context }); // const dispatch = useDispatch(); - return
{joined ? : }
; + return
+ {joined ? : } +
; }; export default Dashboard; \ No newline at end of file diff --git a/src/routes/chat/VoiceChat/JoinVoice.tsx b/src/routes/chat/VoiceChat/JoinVoice.tsx index be65d5c7..d1c54f3c 100644 --- a/src/routes/chat/VoiceChat/JoinVoice.tsx +++ b/src/routes/chat/VoiceChat/JoinVoice.tsx @@ -11,7 +11,7 @@ const JoinVoice = ({ joining, join }: Props) => { Joining
; return ( -
+
Join
); diff --git a/src/routes/chat/VoiceChat/VoiceManagement.tsx b/src/routes/chat/VoiceChat/VoiceManagement.tsx index 686c2624..1644cb3f 100644 --- a/src/routes/chat/VoiceChat/VoiceManagement.tsx +++ b/src/routes/chat/VoiceChat/VoiceManagement.tsx @@ -1,22 +1,49 @@ +import clsx from 'clsx'; import React from 'react'; -import { VoiceInfo } from '../../../app/slices/voice'; +import { VoicingInfo } from '../../../app/slices/voice'; import { useAppSelector } from '../../../app/store'; -import User from '../../../common/component/User'; +import Avatar from '../../../common/component/Avatar'; +import IconMic from '../../../assets/icons/mic.on.svg'; +import IconMicOff from '../../../assets/icons/mic.off.svg'; +// import User from '../../../common/component/User'; type Props = { - info: VoiceInfo | null + info: VoicingInfo | null } const VoiceManagement = ({ info }: Props) => { const userData = useAppSelector(store => store.users.byId); if (!info) return null; const { context, id, members } = info; + const nameClass = clsx(`text-sm text-gray-500 max-w-[190px] truncate font-semibold dark:text-white`); return ( -
-
    +
    +
      {members.map((uid) => { + const curr = userData[uid]; + if (!curr) return null; return
    • - +
      +
      +
      + +
      + + {curr?.name} + +
      +
      + +
      +
      + {/* */} {/* {userData[uid]?.name} */}
    • ; })} diff --git a/src/routes/chat/VoiceChat/index.tsx b/src/routes/chat/VoiceChat/index.tsx index 05b3bdbc..22167cb1 100644 --- a/src/routes/chat/VoiceChat/index.tsx +++ b/src/routes/chat/VoiceChat/index.tsx @@ -23,7 +23,7 @@ const VoiceChat = ({ id, context = "channel" }: Props) => { return (
    • - + {dashboardVisible && }