diff --git a/src/page/Meeting/index.module.scss b/src/page/Meeting/index.module.scss index 1947a60..a05e87b 100644 --- a/src/page/Meeting/index.module.scss +++ b/src/page/Meeting/index.module.scss @@ -111,6 +111,14 @@ >div:nth-child(1) { margin-right: 20px; display: flex; + align-items: center; + cursor: pointer; + + >span { + color: #1677ff; + font-size: 16px; + margin-left: 4px; + } } >div:nth-child(2) { diff --git a/src/page/Meeting/index.tsx b/src/page/Meeting/index.tsx index 96754af..29925d7 100644 --- a/src/page/Meeting/index.tsx +++ b/src/page/Meeting/index.tsx @@ -4,7 +4,7 @@ import Operation from '@/components/Operation'; import SpeakerModeModal from '@/components/SpeakerModeModal'; import InvitingPersonnelModal from '@/components/InvitingPersonnelModal'; import { Button, Input, Popover, Modal, Checkbox, message, Popconfirm, notification } from "antd"; -import { SearchOutlined, EllipsisOutlined, ExclamationCircleFilled, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons'; +import { SearchOutlined, EllipsisOutlined, ExclamationCircleFilled, FullscreenExitOutlined, FullscreenOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { useLocation, useNavigate } from 'react-router-dom'; import { thumbImageBufferToBase64 } from '@/utils/package/base64' import { storage } from '@/utils'; @@ -14,7 +14,7 @@ import { agora } from '@/utils/package/agora' import { onInvoke, onSignalr, offSignalr, onStart } from '@/utils/package/signalr'; import dayjs from 'dayjs'; import durationPlugin from 'dayjs/plugin/duration'; -import { VideoSourceType, VideoStreamType } from 'agora-electron-sdk'; +import { AudioVolumeInfo, QualityType, RtcConnection, RtcStats, UserOfflineReasonType, VideoSourceType, VideoStreamType } from 'agora-electron-sdk'; import Avatar from '@/components/Avatar'; import SharedFilesModel from '@/components/SharedFilesModel'; import StupWizard from '@/components/StupWizard'; @@ -146,7 +146,14 @@ const Meeting: React.FC = () => { const [chatList, setChatList] = useState([]) const [currentVideoId, setCurrentVideoId] = useState('') let [currentSeconds, setCurrentSeconds] = useState(0) - const [currentEffective, setCurrentEffective] = useState(0) + const [currentEffective, setCurrentEffective] = useState(4) + const [currentNetworkInfo, setCurrentNetworkInfo] = useState({ + networkQuality: { + level: '佳', + text: '网络质量极好。' + }, + networkOther: {} as RtcStats + }) const [isComputerAudio, setIsComputerAudio] = useState(true) const [isFluencyPriority, setIsFluencyPriority] = useState(false) const [open, setOpen] = useState(false) @@ -231,14 +238,54 @@ const Meeting: React.FC = () => { }, []); useEffect(() => { - const connection = (navigator as any).connection - if (connection.downlink === 0) { - setCurrentEffective(0) - } else { - let effectiveTypeLength = ['slow-2g', '2g', '3g', '4g'].indexOf((navigator as any).connection.effectiveType) - setCurrentEffective(effectiveTypeLength + 1) + switch (currentEffective) { + case 0: + setCurrentNetworkInfo({ + ...currentNetworkInfo, + networkQuality: { + level: '断开连接', + text: '网络连接断开。' + } + }) + break; + case 1: + setCurrentNetworkInfo({ + ...currentNetworkInfo, + networkQuality: { + level: '非常差', + text: '完全无法沟通。' + } + }) + break; + case 2: + setCurrentNetworkInfo({ + ...currentNetworkInfo, + networkQuality: { + level: '差', + text: '勉强能沟通但不顺畅,网络质量非常差,基本不能沟通。' + } + }) + break; + case 3: + setCurrentNetworkInfo({ + ...currentNetworkInfo, + networkQuality: { + level: '良好', + text: ' 用户主观感受有瑕疵但不影响沟通。' + } + }) + break; + case 4: + setCurrentNetworkInfo({ + ...currentNetworkInfo, + networkQuality: { + level: '佳', + text: '网络质量极好。' + } + }) + break; } - }, [(navigator as any).connection.effectiveType]); + }, [currentEffective]); useEffect(() => { let currentVideoUserItem = roomUserList.find((item: any) => item.uid === currentVideoId || item.screenShareId === currentVideoId) @@ -644,14 +691,14 @@ const Meeting: React.FC = () => { await getJoin(state.enableMicr, state.enableCamera) await agora.init(true) agora.registerEventHandler({ - onJoinChannelSuccess: async (info: any, _elapsed: any) => { - if (info.channelId === state.channelId) { - if (String(info.localUid).length !== 9) { + onJoinChannelSuccess: async (connection: RtcConnection, _elapsed: number) => { + if (connection.channelId === state.channelId) { + if (String(connection.localUid).length !== 9) { setTimeout(async () => { await agora.setupLocalVideo({ - uid: Number(info.localUid), - view: document.getElementById(`video-${info.localUid}`), - channelId: info.channelId, + uid: Number(connection.localUid), + view: document.getElementById(`video-${connection.localUid}`), + channelId: connection.channelId, sourceType: VideoSourceType.VideoSourceCameraPrimary, }) getShowUser(); @@ -659,8 +706,8 @@ const Meeting: React.FC = () => { } } }, - onUserJoined: async (info: any, remoteUid: any, _elapsed: any) => { - if (info.channelId === state.channelId) { + onUserJoined: async (connection: RtcConnection, remoteUid: number, _elapsed: number) => { + if (connection.channelId === state.channelId) { if (String(remoteUid).length === 9) { setIsShare(remoteUid) } else { @@ -668,21 +715,21 @@ const Meeting: React.FC = () => { await agora.setupRemoteVideoJoin({ uid: Number(remoteUid), view: document.getElementById(`video-${remoteUid}`), - channelId: info.channelId, + channelId: connection.channelId, }) }, 1500); } } }, - onUserOffline: async (info: any, remoteUid: any, _reason: any) => { - if (info.channelId === state.channelId) { + onUserOffline: async (connection: RtcConnection, remoteUid: number, _reason: UserOfflineReasonType) => { + if (connection.channelId === state.channelId) { if (String(remoteUid).length === 9) { setIsShare(null) } await agora.setupRemoteVideo({ uid: Number(remoteUid), view: null, - channelId: info.channelId, + channelId: connection.channelId, }); setCurrentVideoId((res: any) => { if (Number(res) === remoteUid) { @@ -692,7 +739,7 @@ const Meeting: React.FC = () => { }) } }, - onAudioVolumeIndication: async (speakers: any) => { + onAudioVolumeIndication: async (speakers: AudioVolumeInfo[]) => { speakers.forEach((item: any) => { let domMe = document.getElementById(`micr-item-${userInfo.uid}`); let dom = document.getElementById(`micr-${item.uid ? item.uid : userInfo.uid}`); @@ -705,6 +752,35 @@ const Meeting: React.FC = () => { domMe.style.height = `${percentage}%` } }); + }, + onNetworkQuality: async (_connection: RtcConnection, remoteUid: number, _txQuality: QualityType, rxQuality: QualityType) => { + if (remoteUid === 0) { + switch (rxQuality) { + case 1: + setCurrentEffective(4) + break; + case 2: + case 3: + setCurrentEffective(3) + break; + case 4: + case 5: + setCurrentEffective(2) + break; + case 6: + setCurrentEffective(1) + break; + default: + setCurrentEffective(navigator.onLine ? 4 : 0) + break; + } + } + }, + onRtcStats: async (stats: RtcStats) => { + setCurrentNetworkInfo({ + ...currentNetworkInfo, + networkOther: stats + }) } }) if (state.enableCamera) { @@ -1550,9 +1626,51 @@ const Meeting: React.FC = () => { {contextHolder}
-
- {networkIcon(currentEffective)} -
+ +
+ 网络质量: + {currentNetworkInfo.networkQuality.level} + + + {currentNetworkInfo.networkQuality.text} + + } + title="" + > + + + +
+
+ 带宽占用: + ↑{currentNetworkInfo.networkOther.txKBitRate}kbps ↓{currentNetworkInfo.networkOther.rxKBitRate}kbps +
+
+ 丢包率: + ↑{currentNetworkInfo.networkOther.txPacketLossRate}% ↓{currentNetworkInfo.networkOther.rxPacketLossRate}% +
+
+ 延迟: + {currentNetworkInfo.networkOther.lastmileDelay}ms +
+
+ } + title="" + trigger="hover" + > +
+ {networkIcon(currentEffective)} + 详情 +
+
{changeCurrentSeconds()}
会议号:{state.channelId} 会议名称:{state.roomName}
@@ -2339,30 +2457,30 @@ const meetingContentError = (item: any) => { const networkIcon = (network: number) => { switch (network) { case 0: - return - - - + return + + + - + case 1: - return - - - - + return + + + + case 2: - return - - - - + return + + + + case 3: return diff --git a/src/utils/package/agora.ts b/src/utils/package/agora.ts index f49bb29..8fdd786 100644 --- a/src/utils/package/agora.ts +++ b/src/utils/package/agora.ts @@ -8,7 +8,12 @@ import { ChannelProfileType, AudioAinsMode, SimulcastStreamMode, - VideoStreamType + VideoStreamType, + QualityType, + RtcConnection, + RtcStats, + AudioVolumeInfo, + UserOfflineReasonType } from "agora-electron-sdk"; import { GetRoomRtcToken, GetAgoraConf } from "@/api/Home/Index"; import { storage } from '@/utils'; @@ -104,19 +109,19 @@ export const agora = { }, 1000); }, // 事件回调 - registerEventHandler: ({ onJoinChannelSuccess, onUserJoined, onUserOffline, onAudioVolumeIndication }: any) => { + registerEventHandler: ({ onJoinChannelSuccess, onUserJoined, onUserOffline, onAudioVolumeIndication, onNetworkQuality, onRtcStats }: any) => { rtcEngine.registerEventHandler({ // 监听本地用户加入频道事件 - onJoinChannelSuccess: async (info: any, elapsed: any) => { - await onJoinChannelSuccess(info, elapsed) + onJoinChannelSuccess: async (connection: RtcConnection, elapsed: number) => { + await onJoinChannelSuccess(connection, elapsed) }, // 监听远端用户加入频道事件 - onUserJoined: async (info: any, remoteUid: any, elapsed: any) => { - await onUserJoined(info, remoteUid, elapsed) + onUserJoined: async (connection: RtcConnection, remoteUid: number, elapsed: number) => { + await onUserJoined(connection, remoteUid, elapsed) }, // 监听用户离开频道事件 - onUserOffline: async (info: any, remoteUid: any, reason: any) => { - await onUserOffline(info, remoteUid, reason) + onUserOffline: async (connection: RtcConnection, remoteUid: number, reason: UserOfflineReasonType) => { + await onUserOffline(connection, remoteUid, reason) }, // // 视频发布状态改变回调 // onVideoPublishStateChanged: (source: any, channel: any, oldState: any, newState: any, elapseSinceLastState: any) => { @@ -131,9 +136,17 @@ export const agora = { // } // }, // // 用户音量提示回调。 - onAudioVolumeIndication: async (_connection: any, speakers: any, _speakerNumber: any, _totalVolume: any,) => { + onAudioVolumeIndication: async (_connection: RtcConnection, speakers: AudioVolumeInfo[], _speakerNumber: number, _totalVolume: number) => { await onAudioVolumeIndication(speakers) - } + }, + //通话中每个用户的网络上下行 last mile 质量报告回调。 + onNetworkQuality: async (connection: RtcConnection, remoteUid: number, txQuality: QualityType, rxQuality: QualityType) => { + await onNetworkQuality(connection, remoteUid, txQuality, rxQuality) + }, + //当前通话相关的统计信息回调。 + onRtcStats: async (_connection: RtcConnection, stats: RtcStats) => { + await onRtcStats(stats) + }, }); }, // 获取视图模式 diff --git a/vite.config.ts b/vite.config.ts index 7487160..325229a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -63,7 +63,12 @@ export default defineConfig({ VideoViewSetupMode, AudioAinsMode, SimulcastStreamMode, - VideoStreamType + VideoStreamType, + QualityType, + RtcConnection, + RtcStats, + AudioVolumeInfo, + UserOfflineReasonType } = require("agora-electron-sdk") export { createAgoraRtcEngine, @@ -75,7 +80,12 @@ export default defineConfig({ VideoViewSetupMode, AudioAinsMode, SimulcastStreamMode, - VideoStreamType + VideoStreamType, + QualityType, + RtcConnection, + RtcStats, + AudioVolumeInfo, + UserOfflineReasonType } `, })