feat: volume and mute indicator

This commit is contained in:
Tristan Yang
2023-03-30 08:45:35 +08:00
parent bd6b02d19a
commit a5e4a198e5
10 changed files with 127 additions and 58 deletions
+1
View File
@@ -117,6 +117,7 @@ export const serverApi = createApi({
url: `group/${id}/agora_token`,
})
}),
// tmp API
getAgoraVoicingList: builder.query<AgoraVoicingListResponse, { appid: string, key: string, secret: string }>({
query: ({ appid, key, secret }) => ({
headers: {
+38 -24
View File
@@ -4,16 +4,29 @@ export type VoiceBasicInfo = {
context: "channel" | "dm"
id: number,
}
export type VoicingInfo = {
downlinkNetworkQuality?: number,
muted: boolean,
members: number[]
} & VoiceBasicInfo
export type VoicingMemberInfo = {
speakingVolume?: number,
muted?: boolean
}
export type VoicingMembers = {
ids: number[],
byId: {
[key: number]: VoicingMemberInfo
}
}
export type VoiceInfo = {
memberCount: number
} & VoiceBasicInfo
interface State {
voicing: VoicingInfo | null,
voicingMembers: VoicingMembers,
list: VoiceInfo[]
}
// const initialInfo = {
@@ -23,6 +36,10 @@ interface State {
// };
const initialState: State = {
voicing: null,
voicingMembers: {
ids: [],
byId: {}
},
list: []
};
@@ -34,13 +51,10 @@ const voiceSlice = createSlice({
reducers: {
updateVoicingInfo(state, { payload }: PayloadAction<VoicingInfo | null>) {
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 = { ...state.voicing, ...payload };
} else {
state.voicing = payload;
}
state.voicing = payload;
},
updateMuteStatus(state, { payload }: PayloadAction<boolean>) {
if (state.voicing) {
@@ -56,30 +70,30 @@ const voiceSlice = createSlice({
state.list = payload;
},
addVoiceMember(state, { payload }: PayloadAction<number>) {
if (state.voicing) {
if (!state.voicing.members.includes(payload)) {
state.voicing.members.push(payload);
}
} else {
// tricky?
state.voicing = {
id: 0,
context: "channel",
muted: false,
members: [payload]
if (!state.voicingMembers.ids.includes(payload)) {
state.voicingMembers.ids.push(payload);
state.voicingMembers.byId[payload] = {
speakingVolume: 0,
muted: false
};
}
},
removeVoiceMember(state, { payload }: PayloadAction<number>) {
if (state.voicing) {
const idx = state.voicing.members.findIndex((uid) => uid == payload);
if (idx > -1) {
state.voicing.members.splice(idx, 1);
}
const idx = state.voicingMembers.ids.findIndex((uid) => uid == payload);
if (idx > -1) {
state.voicingMembers.ids.splice(idx, 1);
delete state.voicingMembers.byId[payload];
}
},
updateVoicingMember(state, { payload }: PayloadAction<{ uid: number, info: VoicingMemberInfo }>) {
const idx = state.voicingMembers.ids.findIndex((uid) => uid == payload.uid);
if (idx > -1) {
const { uid, info } = payload;
state.voicingMembers.byId[uid] = { ...state.voicingMembers.byId[uid], ...info };
}
}
},
});
export const { addVoiceMember, removeVoiceMember, updateVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateMuteStatus } = voiceSlice.actions;
export const { addVoiceMember, removeVoiceMember, updateVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateMuteStatus, updateVoicingMember } = voiceSlice.actions;
export default voiceSlice.reducer;
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg">
<path d="M21.7806 2.21967C22.0735 2.51256 22.0735 2.98744 21.7806 3.28033L18.5609 6.5L21.7806 9.71967C22.0735 10.0126 22.0735 10.4874 21.7806 10.7803C21.4877 11.0732 21.0128 11.0732 20.7199 10.7803L17.5002 7.56066L14.2806 10.7803C13.9877 11.0732 13.5128 11.0732 13.2199 10.7803C12.927 10.4874 12.927 10.0126 13.2199 9.71967L16.4396 6.5L13.2199 3.28033C12.927 2.98744 12.927 2.51256 13.2199 2.21967C13.5128 1.92678 13.9877 1.92678 14.2806 2.21967L17.5002 5.43934L20.7199 2.21967C21.0128 1.92678 21.4877 1.92678 21.7806 2.21967ZM9.36737 3.31232L10.2271 5.33967C10.6018 6.22312 10.3939 7.26203 9.71313 7.90815L7.81881 9.70616C7.93569 10.7816 8.2972 11.8406 8.90334 12.8832C9.50948 13.9257 10.2665 14.7905 11.1744 15.4776L13.4496 14.7189C14.312 14.4313 15.2512 14.7618 15.7802 15.539L17.0125 17.3495C17.6275 18.2529 17.5169 19.4993 16.7538 20.2653L15.9361 21.0862C15.1222 21.9033 13.9597 22.1997 12.8843 21.8643C10.3454 21.0723 8.01109 18.7211 5.88132 14.8107C3.74845 10.8945 2.9957 7.57189 3.62307 4.84289C3.88707 3.69457 4.70458 2.78009 5.77209 2.43899L6.84868 2.09498C7.8575 1.77263 8.93535 2.29358 9.36737 3.31232Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="#70707B" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C17.5228 2 22 6.47715 22 12V19C22 20.6569 20.6569 22 19 22H16C15.4477 22 15 21.5523 15 21V15C15 14.4477 15.4477 14 16 14H20.5V12C20.5 7.30558 16.6944 3.5 12 3.5C7.30558 3.5 3.5 7.30558 3.5 12V14H8C8.55228 14 9 14.4477 9 15V21C9 21.5523 8.55228 22 8 22H5C3.34315 22 2 20.6569 2 19V12C2 6.47715 6.47715 2 12 2Z"/>
<path d="M2.88667 2.21966C3.17956 1.92677 3.65444 1.92678 3.94733 2.21968L22.4471 20.7198C22.74 21.0127 22.74 21.4876 22.4471 21.7805C22.1542 22.0734 21.6793 22.0734 21.3864 21.7805L2.88666 3.28032C2.59377 2.98743 2.59377 2.51255 2.88667 2.21966Z" fill="#D92D20"/>
</svg>

After

Width:  |  Height:  |  Size: 697 B

+3 -9
View File
@@ -1,10 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" stroke="#dc2626" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1027_7306)">
<path d="M1 0.99997L23 23M15 9.33997V3.99997C15.0007 3.256 14.725 2.53829 14.2264 1.98617C13.7277 1.43405 13.0417 1.08691 12.3015 1.01214C11.5613 0.937375 10.8197 1.14031 10.2207 1.58156C9.62172 2.0228 9.20805 2.67088 9.06 3.39997M17 16.95C16.0238 17.9464 14.7721 18.6284 13.4056 18.9086C12.0391 19.1887 10.62 19.0542 9.3305 18.5223C8.04096 17.9903 6.93976 17.0852 6.16817 15.9231C5.39658 14.761 4.98979 13.3949 5 12V9.99997M19 9.99997V12C18.9996 12.4124 18.9628 12.824 18.89 13.23M12 19V23M8 23H16M9 8.99997V12C9.00052 12.5929 9.17675 13.1724 9.50643 13.6653C9.83611 14.1582 10.3045 14.5423 10.8523 14.7691C11.4002 14.996 12.0029 15.0554 12.5845 14.9398C13.1661 14.8243 13.7005 14.539 14.12 14.12L9 8.99997Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_1027_7306">
<rect width="24" height="24" fill="#dc2626"/>
</clipPath>
</defs>
<svg width="24" height="24" viewBox="0 0 24 24" fill="#78787C" xmlns="http://www.w3.org/2000/svg">
<path d="M3.94733 2.21968C3.65444 1.92678 3.17956 1.92677 2.88667 2.21966C2.59377 2.51255 2.59377 2.98743 2.88666 3.28032L8.66699 9.06078V12C8.66699 14.2091 10.4579 16 12.667 16C13.5005 16 14.2744 15.7451 14.9151 15.309L16.061 16.4549C15.1846 17.1112 14.0962 17.5 12.917 17.5H12.417L12.2006 17.4956C9.40144 17.3821 7.16699 15.077 7.16699 12.25V11.75L7.16015 11.6482C7.11048 11.2822 6.79669 11 6.41699 11C6.00278 11 5.66699 11.3358 5.66699 11.75V12.25L5.67105 12.4863C5.78983 15.938 8.50022 18.7316 11.917 18.9818L11.917 21.25L11.9238 21.3518C11.9735 21.7178 12.2873 22 12.667 22C13.0812 22 13.417 21.6642 13.417 21.25L13.418 18.9817C14.8169 18.8791 16.0975 18.35 17.1301 17.5241L21.3864 21.7805C21.6793 22.0734 22.1542 22.0734 22.4471 21.7805C22.74 21.4876 22.74 21.0127 22.4471 20.7198L3.94733 2.21968ZM17.8631 14.0144L19.0091 15.1604C19.4308 14.2791 19.667 13.2921 19.667 12.25V11.75L19.6601 11.6482C19.6105 11.2822 19.2967 11 18.917 11C18.5028 11 18.167 11.3358 18.167 11.75V12.25L18.1626 12.4664C18.1407 13.0075 18.0368 13.5276 17.8631 14.0144ZM8.80467 4.95575L16.5971 12.7483C16.643 12.5059 16.667 12.2558 16.667 12V6C16.667 3.79086 14.8761 2 12.667 2C10.8191 2 9.26391 3.25302 8.80467 4.95575Z" />
<path d="M2.88667 2.21966C3.17956 1.92677 3.65444 1.92678 3.94733 2.21968L22.4471 20.7198C22.74 21.0127 22.74 21.4876 22.4471 21.7805C22.1542 22.0734 21.6793 22.0734 21.3864 21.7805L2.88666 3.28032C2.59377 2.98743 2.59377 2.51255 2.88667 2.21966Z" fill="#D92D20"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+2 -2
View File
@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" stroke="#78787C" xmlns="http://www.w3.org/2000/svg">
<path d="M19 10V12C19 13.8565 18.2625 15.637 16.9497 16.9497C15.637 18.2625 13.8565 19 12 19M12 19C10.1435 19 8.36301 18.2625 7.05025 16.9497C5.7375 15.637 5 13.8565 5 12V10M12 19V23M8 23H16M12 1C11.2044 1 10.4413 1.31607 9.87868 1.87868C9.31607 2.44129 9 3.20435 9 4V12C9 12.7956 9.31607 13.5587 9.87868 14.1213C10.4413 14.6839 11.2044 15 12 15C12.7956 15 13.5587 14.6839 14.1213 14.1213C14.6839 13.5587 15 12.7956 15 12V4C15 3.20435 14.6839 2.44129 14.1213 1.87868C13.5587 1.31607 12.7956 1 12 1Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="#78787C" xmlns="http://www.w3.org/2000/svg">
<path d="M18.25 11C18.6297 11 18.9435 11.2822 18.9932 11.6482L19 11.75V12.25C19 15.8094 16.245 18.7254 12.751 18.9817L12.75 21.25C12.75 21.6642 12.4142 22 12 22C11.6203 22 11.3065 21.7178 11.2568 21.3518L11.25 21.25L11.25 18.9818C7.83323 18.7316 5.12283 15.938 5.00406 12.4863L5 12.25V11.75C5 11.3358 5.33579 11 5.75 11C6.1297 11 6.44349 11.2822 6.49315 11.6482L6.5 11.75V12.25C6.5 15.077 8.73445 17.3821 11.5336 17.4956L11.75 17.5H12.25C15.077 17.5 17.3821 15.2656 17.4956 12.4664L17.5 12.25V11.75C17.5 11.3358 17.8358 11 18.25 11ZM12 2C14.2091 2 16 3.79086 16 6V12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12V6C8 3.79086 9.79086 2 12 2Z" />
</svg>

Before

Width:  |  Height:  |  Size: 675 B

After

Width:  |  Height:  |  Size: 759 B

+5
View File
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="#78787C" xmlns="http://www.w3.org/2000/svg">
<path d="M15 4.25V19.7456C15 20.8243 13.7255 21.3965 12.9194 20.6797L8.42793 16.686C8.29063 16.5639 8.11329 16.4965 7.92956 16.4965H4.25C3.00736 16.4965 2 15.4891 2 14.2465V9.74859C2 8.50595 3.00736 7.49859 4.25 7.49859H7.92961C8.11333 7.49859 8.29065 7.43116 8.42794 7.30909L12.9195 3.31583C13.7255 2.59915 15 3.17138 15 4.25ZM18.9916 5.89733C19.3244 5.65079 19.7941 5.72077 20.0407 6.05362C21.2717 7.7157 22 9.7739 22 12C22 14.2261 21.2717 16.2843 20.0407 17.9464C19.7941 18.2793 19.3244 18.3492 18.9916 18.1027C18.6587 17.8562 18.5888 17.3865 18.8353 17.0536C19.8815 15.6411 20.5 13.8939 20.5 12C20.5 10.1062 19.8815 8.35896 18.8353 6.94641C18.5888 6.61356 18.6587 6.14387 18.9916 5.89733ZM17.143 8.36933C17.5072 8.17214 17.9624 8.30757 18.1596 8.67184C18.6958 9.66245 19 10.7968 19 12C19 13.2032 18.6958 14.3376 18.1596 15.3282C17.9624 15.6924 17.5072 15.8279 17.143 15.6307C16.7787 15.4335 16.6432 14.9783 16.8404 14.6141C17.2609 13.8373 17.5 12.9477 17.5 12C17.5 11.0523 17.2609 10.1627 16.8404 9.38593C16.6432 9.02167 16.7787 8.56652 17.143 8.36933Z" />
<path d="M2.88667 2.21966C3.17956 1.92677 3.65444 1.92678 3.94733 2.21968L22.4471 20.7198C22.74 21.0127 22.74 21.4876 22.4471 21.7805C22.1542 22.0734 21.6793 22.0734 21.3864 21.7805L2.88666 3.28032C2.59377 2.98743 2.59377 2.51255 2.88667 2.21966Z" fill="#D92D20"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+51 -14
View File
@@ -2,13 +2,19 @@ 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, updateMuteStatus, updateVoicingInfo, updateVoicingNetworkQuality } from '../../app/slices/voice';
import { addVoiceMember, removeVoiceMember, updateMuteStatus, updateVoicingInfo, updateVoicingMember, updateVoicingNetworkQuality } from '../../app/slices/voice';
import { useAppSelector } from '../../app/store';
// type Props = {}
window.VOICE_TRACK_MAP = window.VOICE_TRACK_MAP ?? {};
// let tmpUids: number[] = [];
const Voice = () => {
const isAdmin = useAppSelector(store => store.authData.user?.is_admin);
const { isAdmin } = useAppSelector(store => {
return {
isAdmin: !!store.authData.user?.is_admin,
// joined: !!store.voice.voicing
};
});
const { data: agoraConfig } = useGetAgoraConfigQuery(undefined, {
skip: !isAdmin
});
@@ -22,26 +28,48 @@ const Voice = () => {
pollingInterval: 5000
});
const dispatch = useDispatch();
useEffect(() => {
const initializeAgoraClient = async () => {
// Create an instance of the Agora Engine
// 创建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);
// Subscribe and play the remote audio track.
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;
}
// 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");
agoraEngine.on("user-unpublished", (user) => {
// 远端用户取消了音频(muted)
dispatch(updateVoicingMember({ uid: +user.uid, info: { muted: true } }));
});
//remote user leave
agoraEngine.on("user-left", (user, reason) => {
switch (reason) {
case "Quit":
case "ServerTimeOut": {
dispatch(removeVoiceMember(+user.uid as number));
console.log(user + "has left the channel");
}
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) => {
@@ -54,9 +82,11 @@ const Voice = () => {
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:
@@ -68,20 +98,26 @@ const Voice = () => {
});
// 有新用户加入
agoraEngine.on("user-joined", async (user) => {
console.log(user, " has joined the channel");
// console.log(user.uid, !!localTrack, agoraEngine.channelName, " has joined the channel");
// if (localTrack) {
// joined
dispatch(addVoiceMember(+user.uid));
// } else {
// tmpUids.push(+user.uid);
// }
});
window.VOICE_CLIENT = agoraEngine;
};
if (!window.VOICE_CLIENT) {
initializeAgoraClient();
}
return () => {
if (window.VOICE_CLIENT && localTrack) {
localTrack.close();
localTrack = null;
window.VOICE_CLIENT.leave();
}
// window.VOICE_CLIENT=null
};
@@ -118,13 +154,13 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
// Publish the local audio track in the channel.
await window.VOICE_CLIENT.publish(localTrack);
console.log("Publish success!,joined the channel");
dispatch(updateVoicingInfo({
muted: false,
id,
context,
members: [uid]
}));
// 把自己加进去
dispatch(addVoiceMember(uid));
}
}
setJoining(false);
@@ -132,6 +168,7 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
const leave = async () => {
if (window.VOICE_CLIENT && localTrack) {
localTrack.close();
localTrack = null;
await window.VOICE_CLIENT.leave();
dispatch(updateVoicingInfo(null));
}
+6 -5
View File
@@ -2,10 +2,11 @@ import React from 'react';
import { useAppSelector } from '../../app/store';
import User from '../../common/component/User';
import IconHeadphone from '../../assets/icons/headphone.svg';
import IconHeadphoneOff from '../../assets/icons/headphone.off.svg';
import IconMic from '../../assets/icons/mic.on.svg';
import IconMicOff from '../../assets/icons/mic.off.svg';
import IconCallOff from '../../assets/icons/call.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';
@@ -37,7 +38,7 @@ const RTCWidget = ({ id, context = "channel" }: Props) => {
<span className='text-gray-600 dark:text-gray-400 text-xs truncate max-w-[170px]' >{voicingInfo.context == "channel" ? "Channel" : "DM"} / {voicingInfo.context == "channel" ? channelData[voicingInfo.id].name : userData[voicingInfo.id].name}</span>
</div>
</div>
<IconHeadphone onClick={leave} role="button" className="fill-red-600" />
<IconCallOff onClick={leave} role="button" className="fill-red-600" />
</div>
{/* } */}
<div className="flex justify-between items-center">
@@ -50,11 +51,11 @@ const RTCWidget = ({ id, context = "channel" }: Props) => {
</div>
{/* {voicingInfo && */}
<div className="flex gap-2 px-1">
{/* <IconSoundOn role="button" /> */}
<IconHeadphone role="button" />
{voicingInfo.muted ?
<IconMicOff className="w-5" onClick={setMute.bind(null, false)} role="button" />
<IconMicOff onClick={setMute.bind(null, false)} role="button" />
:
<IconMic className="w-5" onClick={setMute.bind(null, true)} role="button" />}
<IconMic onClick={setMute.bind(null, true)} role="button" />}
</div>
{/* } */}
</div>
+14 -4
View File
@@ -12,19 +12,28 @@ type Props = {
}
const VoiceManagement = ({ info }: Props) => {
const userData = useAppSelector(store => store.users.byId);
const { userData, voicingMembers } = useAppSelector(store => {
return {
userData: store.users.byId,
voicingMembers: store.voice.voicingMembers
};
});
if (!info) return null;
const { context, id, members } = info;
const { context, id } = info;
const nameClass = clsx(`text-sm text-gray-500 max-w-[190px] truncate font-semibold dark:text-white`);
const members = voicingMembers.ids;
const membersData = voicingMembers.byId;
return (
<div className='w-full h-full py-2'>
<ul className='flex flex-col gap-2'>
{members.map((uid) => {
const curr = userData[uid];
if (!curr) return null;
const { muted, speakingVolume = 0 } = membersData[uid];
const speaking = speakingVolume > 50;
return <li key={uid}>
<div className="flex items-center justify-between gap-6 ">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 transition-opacity" style={{ opacity: `${speaking ? 0.4 : 1}` }}>
<div className="w-8 h-8 flex shrink-0">
<Avatar
width={32}
@@ -40,7 +49,8 @@ const VoiceManagement = ({ info }: Props) => {
</span>
</div>
<div className="flex items-center">
<IconMic className="w-4" role="button" />
{/* {muted ? <IconMicOff className="w-4" /> : <IconMic className="w-4" />} */}
{muted ? <IconMicOff className="w-4" /> : <IconMic className="w-4" />}
</div>
</div>
{/* <User uid={uid} interactive={false} /> */}