import styles from '@/page/Meeting/index.module.scss' import { useEffect, useRef, useState } from "react"; import Operation from '@/components/Operation'; import SpeakerModeModal from '@/components/SpeakerModeModal'; import InvitingPersonnelModal from '@/components/InvitingPersonnelModal'; import { Button, Input, Popover, Modal, Checkbox, message } from "antd"; import { SearchOutlined } from '@ant-design/icons'; import { useLocation, useNavigate } from 'react-router-dom'; import { thumbImageBufferToBase64 } from '@/utils/package/base64' import { storage } from '@/utils'; import { GetRoomUser, PostOpenMicr, PostOpenCamera, PostRoomManager, DeleteRoomManager, GetRoomKickout, GetShowUser, PostShowUser } from '@/api/Meeting'; import ImageUrl from '@/utils/package/ImageUrl' 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 } from 'agora-electron-sdk'; import { GetUserList } from '@/api/Home/User'; import Avatar from '@/components/Avatar'; import SharedFilesModel from '@/components/SharedFilesModel'; import StupWizard from '@/components/StupWizard'; const fs = require('fs').promises; dayjs.extend(durationPlugin); const Meeting: React.FC = () => { const navigate = useNavigate(); const { state } = useLocation(); const speakerModeModalRef = useRef(); const sharedFilesModelRef = useRef(); const invitingPersonnelRef = useRef(); const stupWizardRef = useRef(); const [statusList, setStatusList] = useState({ userList: false, userChatList: false, }) const [isSharedScreenModal, setIsSharedScreenModal] = useState(false); const [user, setUser] = useState({}); const [sharedScreenList, setSharedScreenList] = useState([]); const [sharedScreenItem, setSharedScreenItem] = useState(''); const [textMsg, setTextMsg] = useState(''); const [footerList, setFooterList] = useState([ [ { title: '关闭声音', icon: ImageUrl.icon22, iconActive: ImageUrl.icon22Active, active: false, }, { title: '关闭视频', icon: ImageUrl.icon23, iconActive: ImageUrl.icon23Active, active: false, }, ], [ { title: '共享屏幕', icon: ImageUrl.icon24, active: false, }, { title: '共享文件', icon: ImageUrl.icon25, active: false, }, { title: '邀请人员', icon: ImageUrl.icon26, active: false, }, { title: '录制', icon: ImageUrl.icon27, iconActive: ImageUrl.icon27Active, active: false, }, { title: '设置向导', icon: ImageUrl.icon28, active: false, }, { title: '结束', icon: ImageUrl.icon29, active: false, }, ], [ { title: '成员列表', icon: ImageUrl.icon30, active: false, }, { title: '聊天', icon: ImageUrl.icon31, active: false, }, ], ]) const [footerListIndex, setFooterListIndex] = useState({ itemIndex: 0, rowIndex: 0, }); const [roomUserList, setRoomUserList] = useState([]) const [allUserList, setAllUserList] = useState([]) const [chatList, setChatList] = useState([]) const [currentVideoId, setCurrentVideoId] = useState('') let [currentSeconds, setCurrentSeconds] = useState(0) const [currentEffective, setCurrentEffective] = useState(0) const [open, setOpen] = useState(false) const [meetingMode, setMeetingMode] = useState('') const [userSearchValue, setUserSearchValue] = useState('') const [noViewChatList, setNoViewChatList] = useState(0) const [currentLookUserAccount, setCurrentLookUserAccount] = useState('') const [currentLookUserStatus, setCurrentLookUserStatus] = useState<1 | 2 | 3>(1) let userInfo = JSON.parse(storage.getItem('user') as string) useEffect(() => { let time = null as any; setUser(userInfo) setMeetingMode('StandardMode'); agora.init(true) agora.registerEventHandler({ onJoinChannelSuccess: async (info: any, _elapsed: any) => { if (String(info.localUid).length !== 9) { await onInvoke('joinChannel', { roomNum: info.channelId, enableMicr: true, enableCamera: true }) await getRoomUser() setTimeout(async () => { let view = document.getElementById(`video-${info.localUid}`) as HTMLElement; if (view && !view.getAttribute('load')) { await agora.setupLocalVideo({ uid: Number(info.localUid), view, channelId: info.channelId, sourceType: VideoSourceType.VideoSourceCameraPrimary, }) view.setAttribute('load', 'true') } getShowUser(); }, 1000); } }, onUserJoined: async (info: any, remoteUid: any, _elapsed: any) => { if (String(remoteUid).length !== 9) { await getRoomUser() setTimeout(async () => { let view = document.getElementById(`video-${remoteUid}`) as HTMLElement; if (view && !view.getAttribute('load')) { await agora.setupRemoteVideoJoin({ uid: Number(remoteUid), view, channelId: info.channelId, }) view.setAttribute('load', 'true') } }, 1000); } }, onUserOffline: async (info: any, remoteUid: any, _reason: any) => { await agora.setupRemoteVideo({ uid: Number(remoteUid), view: null, channelId: info.channelId, }) setTimeout(() => { getRoomUser() }, 1000); } }) agora.startCameraCapture() agora.setJoinChannel({ channelId: state.channelId, uid: userInfo.uid, token: state.token, }) setCurrentVideoId(userInfo.uid) storage.setItem('noViewChatList', 0) window.addEventListener('customStorageChange', handleCustomStorageChange); window.addEventListener('online', handleNetworkChange); window.addEventListener('offline', handleNetworkChange); time = setInterval(() => { setCurrentSeconds(currentSeconds++) }, 1000) return () => { window.removeEventListener('customStorageChange', handleCustomStorageChange); window.removeEventListener('online', handleNetworkChange); window.removeEventListener('offline', handleNetworkChange); clearInterval(time) }; }, []); useEffect(() => { roomUserList.forEach((item: any) => { if (item.uid === user.uid) { const footerListTemplate = [...footerList] footerListTemplate[0][0].title = item.enableMicr ? '关闭声音' : '开启声音' footerListTemplate[0][0].active = !item.enableMicr footerListTemplate[0][1].title = item.enableCamera ? '关闭视频' : '开启视频' footerListTemplate[0][1].active = !item.enableCamera setFooterList(footerListTemplate) } agora.muteLocalAudioStream(!item.enableMicr) agora.muteLocalVideoStream(!item.enableCamera) }); }, [roomUserList]); 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) } }, [(navigator as any).connection.effectiveType]); useEffect(() => { onSignalr((item: any) => { switch (item.key) { case 'ReceiveMessage': let meetingUserChatDom = document.getElementById('meetingUserChat') as HTMLElement; if (!meetingUserChatDom) { let storageNoViewChatList = Number(storage.getItem('noViewChatList')) storage.setItem('noViewChatList', storageNoViewChatList += 1) setNoViewChatList(storageNoViewChatList) } setChatList((newChatList: any) => [...newChatList, item]) break; case 'RefreshUserList': getRoomUser() break; case 'Operation': // 1:全员退出会议 // 2:设置取消管理员 // 3:踢出房间 switch (item.type) { case 1: leaveChannel() break; case 2: case 3: getRoomUser() break; } break; case 'ForceExitRoom': leaveChannel() break; case 'RefreshView': setMeetingMode(item.type) break; case 'ShowUser': getShowUser() break; } }) return () => { offSignalr() } }, []) useEffect(() => { }, [currentVideoId]) // 网络 const handleNetworkChange = (): void => { if (navigator.onLine) { message.success('网络已恢复。') setTimeout(async () => { await onStart(async () => { await onInvoke('joinChannel', { roomNum: state.channelId, enableMicr: true, enableCamera: true }) await getRoomUser() }) }, 1000) } else { message.error('网络已断开!') } } // 全员观看 const getShowUser = async (): Promise => { await GetShowUser(state.channelId).then(async (res) => { if (res.code === 200 && res.data) { setCurrentLookUserAccount(res.data) if (res.data === userInfo.uid) { if (String(res.data).length === 9) { // 共享屏幕 setCurrentLookUserStatus(2) setTimeout(() => { agora.setupLocalVideo({ uid: Number(res.data), view: document.getElementById(`video-source-screen`) as HTMLElement, channelId: state.channelId, sourceType: VideoSourceType.VideoSourceScreen, }) }, 1000); } else { setCurrentLookUserStatus(1) // 摄像头 setTimeout(() => { agora.setupLocalVideo({ uid: Number(res.data), view: document.getElementById(`video-source-camera-primary`) as HTMLElement, channelId: state.channelId, sourceType: VideoSourceType.VideoSourceCameraPrimary, }) }, 1000); } } else { setCurrentLookUserStatus(3) setTimeout(() => { agora.setupRemoteVideoJoin({ uid: Number(res.data), view: document.getElementById(`video-source-remote`) as HTMLElement, channelId: state.channelId, }) }, 1000); } } }) } // 加入房间时间 const changeCurrentSeconds = (): string => { const duration = dayjs.duration(currentSeconds, 'seconds'); const hours = duration.hours(); // 整数小时 const minutes = duration.minutes(); // 整数分钟 const secondsRemaining = duration.seconds(); // 剩余的秒数 return `${hours > 9 ? hours : '0' + hours}:${minutes > 9 ? minutes : '0' + minutes}:${secondsRemaining > 9 ? secondsRemaining : '0' + secondsRemaining}` } // 操作按钮 const changeStatusList = async (row: any, itemIndex: number, rowIndex: number): Promise => { const footerListTemplate = [...footerList] setFooterListIndex({ itemIndex, rowIndex, }) switch (row.title) { case '成员列表': setStatusList({ userList: statusList.userList ? false : true, userChatList: false, }) storage.setItem('noViewChatList', 0) setNoViewChatList(0) break; case '聊天': setStatusList({ userList: false, userChatList: statusList.userChatList ? false : true, }) storage.setItem('noViewChatList', 0) setNoViewChatList(0) break; case '共享屏幕': getDesktopCapturerVideo() setIsSharedScreenModal(true) break; case '停止共享': agora.stopScreenCapture() await allUserLook(user.uid) footerListTemplate[itemIndex][rowIndex].title = '共享屏幕' break; case '关闭声音': footerListTemplate[itemIndex][rowIndex].title = '开启声音' footerListTemplate[itemIndex][rowIndex].active = true setFooterList(footerListTemplate) postOpenMicr(false, user.uid) break; case '开启声音': footerListTemplate[itemIndex][rowIndex].title = '关闭声音' footerListTemplate[itemIndex][rowIndex].active = false setFooterList(footerListTemplate) postOpenMicr(true, user.uid) break; case '关闭视频': footerListTemplate[itemIndex][rowIndex].title = '开启视频' footerListTemplate[itemIndex][rowIndex].active = true setFooterList(footerListTemplate) await postOpenCamera(false, user.uid) await agora.stopCameraCapture(); break; case '开启视频': footerListTemplate[itemIndex][rowIndex].title = '关闭视频' footerListTemplate[itemIndex][rowIndex].active = false setFooterList(footerListTemplate) await postOpenCamera(true, user.uid) await agora.startCameraCapture() break; case '设置向导': // stupWizardRef.current.changeModal() break; case '邀请人员': invitingPersonnelRef.current.changeInvitingPersonnelModal() break; case '录制': const setting = await JSON.parse(storage.getItem('setting') as string) if (currentVideoId === user.uid) { message.error('请勿自己录制自己!') } else { try { await fs.access(setting.recordingFilesPath, fs.constants.F_OK) footerListTemplate[itemIndex][rowIndex].title = '录制中' footerListTemplate[itemIndex][rowIndex].active = true setFooterList(footerListTemplate) agora.startRecording(Number(currentVideoId)) } catch (error: any) { if (error.code === 'ENOENT') { message.error('文件夹不存在!') return } else { message.error(error) } } } break; case '录制中': footerListTemplate[itemIndex][rowIndex].title = '录制' footerListTemplate[itemIndex][rowIndex].active = false setFooterList(footerListTemplate) agora.stopRecording() break; case '共享文件': sharedFilesModelRef.current.getData() break; } } // 退出房间 const leaveChannel = async (bool?: boolean): Promise => { if (!bool) { await onInvoke('levelChannel', { roomNum: state.channelId }) } agora.leaveChannel() navigate('/home/index') } // 分享屏幕 const clickSharedScreen = async (): Promise => { let data = sharedScreenList.find((item: any) => item.sourceId === sharedScreenItem.sourceId) if (data) { const footerListTemplate = [...footerList] footerListTemplate[footerListIndex.itemIndex][footerListIndex.rowIndex].title = '停止共享' setIsSharedScreenModal(false) await agora.setDesktopCapturerVideo(sharedScreenItem) await allUserLook(user.screenShareId) } else { message.error('请选择应用!') } } // 获取桌面可共享屏幕的引用 const getDesktopCapturerVideo = (): void => { agora.getDesktopCapturerVideo().then((res: any) => { if (sharedScreenList.length !== res.length) { res.forEach((item: any) => { if (item.thumbImage.buffer) { item.thumbnailUrl = thumbImageBufferToBase64(item.thumbImage) } if (item.iconImage.buffer) { item.iconDataUrl = thumbImageBufferToBase64(item.iconImage) } }) setSharedScreenList(res) } }) }; // 设置全员看谁 const allUserLook = async (uid: string): Promise => { await PostShowUser(state.channelId, uid).then(res => { if (res.code === 200) { getShowUser() } }) } // 获取房间用户 const getRoomUser = async (): Promise => { Promise.all([ GetRoomUser(state.channelId), GetUserList({ pageIndex: 1, pageSize: 9999, searchKeywod: '', isOnline: true, }) ]).then(res => { if (res[0].code === 200 && res[1].code === 200) { setRoomUserList(res[0].data.map((item: any) => { return { ...item, isShow: true, uid: item.id, } })) res[1].data.items.forEach((item: any) => { item.uid = item.id; const itemUser = res[0].data.find((row: any) => row.id === item.id) if (itemUser) { item.isRoom = true; for (const itemUserKey in itemUser) { for (const key in item) { if (itemUserKey === key) { item[key] = itemUser[itemUserKey]; } } } } else { item.isRoom = false; } }); setAllUserList(res[1].data.items); } }) } const handleCustomStorageChange = async (e: any): Promise => { switch (e.key) { case 'meetingMode': setMeetingMode(e.value) break; case 'reconnect': if (e.value == true) { storage.setItem('reconnect', false) await onInvoke('joinChannel', { roomNum: state.channelId, enableMicr: true, enableCamera: true }) await getRoomUser() } break; } }; // 聊天发送 const sendMsg = (): void => { if (textMsg) { onInvoke('sendChannelMsg', { roomNum: state.channelId, msg: textMsg, }) setChatList((newChatList: any) => [...newChatList, { uid: state.uid, userName: user.userName, message: textMsg, }]) setTextMsg('') } else { message.error('请输入内容!') } } // 开关麦克风 const postOpenMicr = async (enableMicr: boolean, uid: string, isAll?: boolean): Promise => { await PostOpenMicr({ roomNum: state.channelId, uid, enableMicr, isAll, }) } // 开关视频 const postOpenCamera = async (enableCamera: boolean, uid: string): Promise => { await PostOpenCamera({ roomNum: state.channelId, uid, enableCamera }) } // 演讲者模式 const changeSpeakerMode = (): void => { speakerModeModalRef.current.changeSpeakerMode() } // 获取当前模式样式 const getMeetingContentBodyLeftModeClass = (): string => { switch (meetingMode) { case 'FreedomMode': return styles.meetingContentBodyLeftFreedomMode case 'StandardMode': return styles.meetingContentBodyLeftStandardMode case 'SpeakerMode': return styles.meetingContentBodyLeftSpeakerMode case 'SingleScreenMode': return styles.meetingContentBodyLeftSingleScreenMode case 'DualScreenMode': return styles.meetingContentBodyLeftDualScreenMode case 'FourScreenMode': return styles.meetingContentBodyLeftFourScreenMode } return '' } // 获取当前模式文字 const getMeetingContentBodyLeftModeText = (): string => { switch (meetingMode) { case 'FreedomMode': return '自由者模式' case 'StandardMode': return '标准模式' case 'SpeakerMode': return '演讲者模式' case 'SingleScreenMode': return '单画面模式' case 'DualScreenMode': return '二分屏模式' case 'FourScreenMode': return '四分屏模式' } return '' } // 设置单个视频样式 const setMeetingContentSwiperCardClass = (uid: string, bool: boolean = false): string => { if ((bool || currentVideoId === uid) && (meetingMode === 'StandardMode' || meetingMode === 'SpeakerMode')) { switch (meetingMode) { case 'StandardMode': return styles.meetingContentSwiperCardStandardMode case 'SpeakerMode': return styles.meetingContentSwiperCardSpeakerMode } } return '' } return ( <>
{networkIcon(currentEffective)}
{changeCurrentSeconds()}
会议号:{state.channelId}
{getMeetingContentBodyLeftModeText()}
{/* ${setMeetingContentSwiperCardClass(item.uid)} */} {allUserList.map((item: any, index: number) => { return ( item.isRoom ?
{ if (footerList[1][3].active) { return message.error('视频录制中请勿切换,或结束录制再切换!') } setCurrentVideoId(item.uid) }} >
{meetingContentUser(item)} {item.enableCamera ? null : meetingContentError(currentVideoId, item)}
: null ) } )} {currentLookUserStatus === 1 ?
暂无视频
: null} {currentLookUserStatus === 2 ?
暂无视频
: null} {currentLookUserStatus === 3 ?
暂无视频
: null}
{ (statusList.userList || statusList.userChatList) ? (
{statusList.userList ?
成员列表 { setStatusList({ userList: false, userChatList: false, }) }} />
} value={userSearchValue} onChange={(e) => { setUserSearchValue(e.target.value) const newRoomUserList = [...roomUserList] newRoomUserList.forEach(row => { if (e.target.value) { if (row.userName.indexOf(e.target.value) !== -1) { row.isShow = true; } else { row.isShow = false; } } else { row.isShow = true; } }); setRoomUserList(newRoomUserList) }} />
{roomUserList.map((item: any, index: number) => { return ( item.isShow ?
{item.userName} {item.roleId === '1' || item.isManager ? {item.roleId === '1' ? '主持人' : '临时主持人'} : null}
{ postOpenMicr(!item.enableMicr, item.id) }} title={item.enableMicr ? '关闭声音' : '打开声音'} /> { postOpenCamera(!item.enableCamera, item.id) }} title={item.enableCamera ? '关闭视频' : '打开视频'} />
{item.uid !== user.uid && user.roleId === '1' ?
{!item.isManager ? : }
: null}
: null ) } )}
invitingPersonnelRef.current.changeInvitingPersonnelModal()}>邀请
postOpenMicr(false, user.id, true)}>全员静音
:
聊天 { setStatusList({ userList: false, userChatList: false, }) }} />
{chatList.map((item: any, index: number) =>
{item.userName}
{item.message}
)}
{ setTextMsg(e.target.value) }}>
}
) : null }
{footerList.map((item, itemIndex) => { return (
{item.map((row, rowIndex) => { return ( row.title === '结束' ?
{ await onInvoke('sendOper', { roomNum: state.channelId, type: 1, }) leaveChannel(true) }}>全员结束会议
leaveChannel()}>仅自己离开
{ setOpen(false) }}>取消
} title="" trigger="click" open={open} onOpenChange={() => setOpen(true)} >
{row.title}
:
changeStatusList(row, itemIndex, rowIndex)} key={rowIndex}> {row.title} {row.title === '成员列表' ?
{roomUserList.length}
: null} {row.title === '聊天' && noViewChatList > 0 ?
{noViewChatList}
: null}
) })}
) })}
{sharedScreenList.map((item: any, index: number) => { return (
{ setSharedScreenItem(item) }}> {item.iconDataUrl ? : ''}
{item.sourceTitle}
) })}
{ }}>共享电脑音频
) } const meetingContentUser = (item: any) => { return ( <>
{item.roleId === '1' || item.isManager ?
: null}
{item.userName}
) } const meetingContentError = (currentVideoId: any, item: any) => { return ( <>
) } const networkIcon = (network: number) => { switch (network) { case 0: return case 1: return case 2: return case 3: return case 4: return } } export default Meeting