import { useState, useEffect, useRef } from 'react'; import { useParams } from 'react-router'; import { User } from '../../../share/types.ts'; import { ClassroomAPI } from '../../api/index.ts'; // @ts-types="../../../share/aliyun-rtc-sdk.d.ts" import AliRtcEngine, { AliRtcSubscribeState, AliRtcVideoTrack } from 'aliyun-rtc-sdk'; import { toast } from 'react-toastify'; export enum Role { Teacher = 'admin', Student = 'student' } // 从SDK中提取需要的类型和枚举 type ImEngine = InstanceType; type ImGroupManager = AliVCInteraction.AliVCIMGroupManager; type ImMessageManager = AliVCInteraction.AliVCIMMessageManager; type ImLogLevel = AliVCInteraction.ImLogLevel; type ImMessageLevel = AliVCInteraction.ImMessageLevel; const { ERROR } = AliVCInteraction.ImLogLevel; const { NORMAL, HIGH } = AliVCInteraction.ImMessageLevel; interface ImUser { userId: string; userExtension?: string; } interface ImGroupMessage { groupId: string; type: number; data: string; sender?: ImUser; timestamp?: number; } // 互动消息类型 enum InteractionAction { HandUp = 'hand_up', CancelHandUp = 'cancel_hand_up', AnswerHandUp = 'answer_hand_up' } interface InteractionMessage { action: InteractionAction; studentId: string; studentName?: string; timestamp?: number; question?: string; } interface HandUpRequest { studentId: string; studentName?: string; timestamp: number; question?: string; } interface Question { studentId: string; studentName?: string; question: string; timestamp: number; } export enum ClassStatus { NOT_STARTED = 'not_started', IN_PROGRESS = 'in_progress', ENDED = 'ended' } export const useClassroom = ({ user }:{ user : User }) => { // 状态管理 // const [userId, setUserId] = useState(''); // 保持string类型 const userId = user.id.toString(); const [isCameraOn, setIsCameraOn] = useState(false); const [isAudioOn, setIsAudioOn] = useState(false); const [isScreenSharing, setIsScreenSharing] = useState(false); const [className, setClassName] = useState(''); const [role, setRole] = useState(); const [classId, setClassId] = useState(''); const [isLoggedIn, setIsLoggedIn] = useState(false); const [isJoinedClass, setIsJoinedClass] = useState(false); const [msgText, setMsgText] = useState(''); const [messageList, setMessageList] = useState([]); const [errorMessage, setErrorMessage] = useState(''); const [classStatus, setClassStatus] = useState(ClassStatus.NOT_STARTED); const [handUpList, setHandUpList] = useState([]); const [questions, setQuestions] = useState([]); const [students, setStudents] = useState>([]); const [shareLink, setShareLink] = useState(''); const [showCameraOverlay, setShowCameraOverlay] = useState(true); // SDK实例 const imEngine = useRef(null); const imGroupManager = useRef(null); const imMessageManager = useRef(null); const aliRtcEngine = useRef(null); const remoteVideoElMap = useRef>({}); const remoteScreenContainer = useRef(null); // 主屏幕共享容器(重命名) const remoteCameraContainer = useRef(null); // 摄像头小窗容器 // 辅助函数 const showMessage = (text: string): void => { setMessageList((prevMessageList) => [...prevMessageList, text]) }; const showToast = (type: 'info' | 'success' | 'error', message: string): void => { toast[type](message); }; // 事件监听函数 const listenImEvents = (): void => { if (!imEngine.current) return; if (!role) return; imEngine.current.on('connectsuccess', () => { showMessage('IM连接成功'); }); imEngine.current.on('disconnect', async (code: number) => { showMessage(`IM断开连接: ${code}`); // 自动重连 try { const { token, nonce, timestamp } = await ClassroomAPI.getIMToken(userId, role); await imEngine.current!.login({ user: { userId, userExtension: JSON.stringify(user) }, userAuth: { nonce, timestamp, token, role } }); showMessage('IM自动重连成功'); } catch (err: unknown) { const error = err as Error; showMessage(`IM自动重连失败: ${error.message}`); } }); }; const listenGroupEvents = (): void => { if (!imGroupManager.current) return; imGroupManager.current.on('memberchange', (groupId: string, memberCount: number, joinUsers: ImUser[], leaveUsers: ImUser[]) => { showMessage(`成员变更: 加入${joinUsers.length}人, 离开${leaveUsers.length}人`); }); }; const listenMessageEvents = (): void => { if (!imMessageManager.current) return; imMessageManager.current.on('recvgroupmessage', (msg: AliVCInteraction.ImMessage, groupId: string) => { if (msg.type === 88889) { // 课堂状态消息 try { const data = JSON.parse(msg.data); if (data.action === 'start_class') { setClassStatus(ClassStatus.IN_PROGRESS); showMessage('老师已开始上课'); } else if (data.action === 'end_class') { setClassStatus(ClassStatus.ENDED); showMessage('老师已结束上课'); } } catch (err) { console.error('解析课堂状态消息失败', err); } } else if (msg.type === 88890) { // 静音指令 try { const data = JSON.parse(msg.data); if (data.action === 'toggle_mute' && data.userId === userId) { showMessage(data.mute ? '你已被老师静音' : '老师已取消你的静音'); } } catch (err) { console.error('解析静音指令失败', err); } } else if (msg.type === 88891) { // 举手消息 try { const data = JSON.parse(msg.data) as InteractionMessage; if (data.action === InteractionAction.HandUp) { const handUpData: HandUpRequest = { ...data, timestamp: data.timestamp || Date.now() }; setHandUpList([...handUpList, handUpData]); showMessage(`${data.studentName || data.studentId} 举手了`); } else if (data.action === InteractionAction.CancelHandUp) { setHandUpList(handUpList.filter(h => h.studentId !== data.studentId)); } } catch (err) { console.error('解析举手消息失败', err); } } else if (msg.type === 88892) { // 问题消息 try { const data = JSON.parse(msg.data) as {question: string}; if (typeof data.question === 'string') { const question: Question = { studentId: msg.sender?.userId || 'unknown', studentName: (() => { try { return msg.sender?.userExtension ? JSON.parse(msg.sender.userExtension)?.nickname : null; } catch { return null; } })() || msg.sender?.userId || '未知用户', question: data.question, timestamp: msg.timestamp || Date.now() }; setQuestions([...questions, question]); } showMessage(`收到问题: ${data.question}`); } catch (err) { console.error('解析问题消息失败', err); } } else if (msg.type === 88893) { // 应答消息 try { const data = JSON.parse(msg.data) as InteractionMessage; if (data.action === InteractionAction.AnswerHandUp && data.studentId === userId) { showMessage('老师已应答你的举手'); setHandUpList(handUpList.filter(h => h.studentId !== data.studentId)); } } catch (err) { console.error('解析应答消息失败', err); } } else if (msg.type === 88888) { // 普通文本消息 const sender = msg.sender; const userExtension = JSON.parse(sender?.userExtension || '{}') as User; const senderName = userExtension.nickname || userExtension.username; showMessage(`${ senderName || '未知用户' }: ${msg.data}`); } }); }; // RTC相关函数 const removeRemoteVideo = (userId: string, type: 'camera' | 'screen' = 'camera') => { const vid = `${type}_${userId}`; const el = remoteVideoElMap.current[vid]; if (el) { aliRtcEngine.current!.setRemoteViewConfig(null, userId, type === 'camera' ? AliRtcVideoTrack.AliRtcVideoTrackCamera : AliRtcVideoTrack.AliRtcVideoTrackScreen); el.pause(); // 根据流类型从不同容器移除 if (type === 'camera') { remoteCameraContainer.current?.removeChild(el); } else { remoteScreenContainer.current?.removeChild(el); } delete remoteVideoElMap.current[vid]; } }; const listenRtcEvents = () => { if (!aliRtcEngine.current) return; showMessage('注册rtc事件监听') aliRtcEngine.current.on('remoteUserOnLineNotify', (userId: string) => { showMessage(`用户 ${userId} 加入课堂`); console.log('用户上线通知:', userId); }); aliRtcEngine.current.on('remoteUserOffLineNotify', (userId: string) => { showMessage(`用户 ${userId} 离开课堂`); console.log('用户下线通知:', userId); removeRemoteVideo(userId, 'camera'); removeRemoteVideo(userId, 'screen'); }); aliRtcEngine.current.on('videoSubscribeStateChanged', ( userId: string, oldState: AliRtcSubscribeState, newState: AliRtcSubscribeState, interval: number, channelId: string ) => { console.log(`视频订阅状态变化: 用户 ${userId}, 旧状态 ${oldState}, 新状态 ${newState}`); switch(newState) { case 3: // 订阅成功 try { console.log('开始创建远程视频元素'); if (remoteVideoElMap.current[`camera_${userId}`]) { console.log(`用户 ${userId} 的视频元素已存在`); return; } const video = document.createElement('video'); video.autoplay = true; video.playsInline = true; video.className = 'w-80 h-45 mr-2 mb-2 bg-black'; if (!remoteCameraContainer.current) { console.error('摄像头视频容器未找到'); return; } remoteCameraContainer.current.appendChild(video); remoteVideoElMap.current[`camera_${userId}`] = video; aliRtcEngine.current!.setRemoteViewConfig( video, userId, AliRtcVideoTrack.AliRtcVideoTrackCamera ); console.log(`已订阅用户 ${userId} 的视频流`); showMessage(`已显示用户 ${userId} 的视频`); } catch (err) { console.error(`订阅用户 ${userId} 视频流失败:`, err); showMessage(`订阅用户 ${userId} 视频流失败`); } break; case 1: // 取消订阅 console.log(`取消订阅用户 ${userId} 的视频流`); showMessage(`取消订阅用户 ${userId} 的视频流`); removeRemoteVideo(userId, 'camera'); break; case 2: // 订阅中 console.log(`正在订阅用户 ${userId} 的视频流...`); break; default: console.warn(`未知订阅状态: ${newState}`); } }); aliRtcEngine.current.on('screenShareSubscribeStateChanged', ( userId: string, oldState: AliRtcSubscribeState, newState: AliRtcSubscribeState, elapseSinceLastState: number, channel: string ) => { console.log(`屏幕分享订阅状态变更:uid=${userId}, oldState=${oldState}, newState=${newState}`); switch(newState) { case 3: // 订阅成功 try { console.log('开始创建屏幕分享视频元素'); if (remoteVideoElMap.current[`screen_${userId}`]) { console.log(`用户 ${userId} 的屏幕分享元素已存在`); return; } const video = document.createElement('video'); video.autoplay = true; video.playsInline = true; video.className = 'w-full h-full bg-black'; if (!remoteScreenContainer.current) { console.error('屏幕共享容器未找到'); return; } remoteScreenContainer.current.appendChild(video); remoteVideoElMap.current[`screen_${userId}`] = video; aliRtcEngine.current!.setRemoteViewConfig( video, userId, AliRtcVideoTrack.AliRtcVideoTrackScreen ); console.log(`已订阅用户 ${userId} 的屏幕分享流`); showMessage(`已显示用户 ${userId} 的屏幕分享`); } catch (err) { console.error(`订阅用户 ${userId} 屏幕分享流失败:`, err); showMessage(`订阅用户 ${userId} 屏幕分享流失败`); } break; case 1: // 取消订阅 console.log(`取消订阅用户 ${userId} 的屏幕分享流`); showMessage(`取消订阅用户 ${userId} 的屏幕分享流`); removeRemoteVideo(userId, 'screen'); break; case 2: // 订阅中 console.log(`正在订阅用户 ${userId} 的屏幕分享流...`); break; default: console.warn(`未知屏幕分享订阅状态: ${newState}`); } }); }; // 课堂操作方法 const login = async (role: Role): Promise => { if(!role) { showToast('error', '角色不存在'); return; } try { const { ImEngine: ImEngineClass } = window.AliVCInteraction; const {appId, appSign, timestamp, nonce, token} = await ClassroomAPI.getIMToken(userId, role); imEngine.current = ImEngineClass.createEngine(); await imEngine.current.init({ deviceId: 'xxxx', appId, appSign, logLevel: ERROR, }); await imEngine.current.login({ user: { userId, userExtension: JSON.stringify({ nickname: user?.nickname || user?.username || '' }) }, userAuth: { nonce, timestamp, token, role } }); aliRtcEngine.current = AliRtcEngine.getInstance(); AliRtcEngine.setLogLevel(0); listenImEvents(); listenRtcEvents(); setIsLoggedIn(true); setErrorMessage(''); showToast('success', '登录成功'); } catch (err: any) { setErrorMessage(`登录失败: ${err.message}`); showToast('error', '登录失败'); } }; const joinClass = async (classId: string): Promise => { if (!imEngine.current || !aliRtcEngine.current) return; // // 优先使用URL参数中的classId和role // const { id: pathClassId, role: pathRole } = useParams(); // const finalClassId = (classId || pathClassId) as string; // if (pathRole && ['teacher', 'student'].includes(pathRole)) { // setRole(pathRole === 'teacher' ? Role.Teacher : Role.Student); // } // if (!finalClassId) { // setErrorMessage('课堂ID不能为空'); // showToast('error', '请输入有效的课堂ID'); // return; // } try { const gm = imEngine.current.getGroupManager(); const mm = imEngine.current.getMessageManager(); imGroupManager.current = gm || null; imMessageManager.current = mm || null; await gm!.joinGroup(classId); listenGroupEvents(); listenMessageEvents(); await joinRtcChannel(classId); buildShareLink(classId) setIsJoinedClass(true); setErrorMessage(''); showToast('success', '加入课堂成功'); } catch (err: any) { setErrorMessage(`加入课堂失败: ${err.message}`); showToast('error', '加入课堂失败'); if (imGroupManager.current) { try { await imGroupManager.current.leaveGroup(classId); } catch (leaveErr) { console.error('离开IM群组失败:', leaveErr); } } } }; const leaveClass = async (): Promise => { try { if (imGroupManager.current && classId) { await imGroupManager.current.leaveGroup(classId); } if (aliRtcEngine.current) { await leaveRtcChannel(); } setIsJoinedClass(false); showToast('info', '已离开课堂'); } catch (err) { console.error('离开课堂失败:', err); showToast('error', '离开课堂时发生错误'); } }; const sendMessage = async (): Promise => { if (!imMessageManager.current || !classId) return; if (!msgText.trim()) { showToast('error', '消息不能为空'); return; } try { await imMessageManager.current.sendGroupMessage({ groupId: classId, data: msgText, type: 88888, level: NORMAL, }); setMsgText(''); setErrorMessage(''); } catch (err: any) { setErrorMessage(`消息发送失败: ${err.message}`); } }; const startClass = async (): Promise => { if (!imMessageManager.current || !classId || role !== Role.Teacher) return; try { await imMessageManager.current.sendGroupMessage({ groupId: classId, data: JSON.stringify({ action: 'start_class' }), type: 88889, level: HIGH, }); setClassStatus(ClassStatus.IN_PROGRESS); showToast('success', '课堂已开始'); } catch (err: any) { setErrorMessage(`开始上课失败: ${err.message}`); } }; const endClass = async (): Promise => { if (!imMessageManager.current || !classId || role !== Role.Teacher) return; try { await imMessageManager.current.sendGroupMessage({ groupId: classId, data: JSON.stringify({ action: 'end_class' }), type: 88889, level: HIGH, }); setClassStatus(ClassStatus.ENDED); showToast('success', '课堂已结束'); try { await leaveRtcChannel(); } catch (err: any) { console.error('离开RTC频道失败:', err); showToast('error', '离开RTC频道失败'); } } catch (err: any) { setErrorMessage(`结束上课失败: ${err.message}`); } }; const toggleMuteMember = async (userId: string, mute: boolean): Promise => { if (!imMessageManager.current || !classId || role !== Role.Teacher) return; try { await imMessageManager.current.sendGroupMessage({ groupId: classId, data: JSON.stringify({ action: 'toggle_mute', userId, mute }), type: 88890, level: HIGH, }); showToast('info', mute ? `已静音用户 ${userId}` : `已取消静音用户 ${userId}`); } catch (err: any) { setErrorMessage(`操作失败: ${err.message}`); } }; const buildShareLink = (classId: string) => { const getBaseUrl = () => { const protocol = window.location.protocol; const host = window.location.host; return `${protocol}//${host}`; } // const baseUrl = window.location.href.split('?')[0].replace(/\/[^/]*$/, ''); const baseUrl = getBaseUrl(); setShareLink(`${baseUrl}/mobile/classroom/${classId}/student`); } const createClass = async (className: string, maxMembers = 200): Promise => { if (!imEngine.current || !isLoggedIn || role !== Role.Teacher) { showToast('error', '只有老师可以创建课堂'); return null; } try { const groupManager = imEngine.current.getGroupManager(); if (!groupManager) { throw new Error('群组管理器未初始化'); } showToast('info', '正在创建课堂...'); const response = await groupManager.createGroup({ groupName: className, groupMeta: JSON.stringify({ classType: 'interactive', creator: userId, createdAt: Date.now(), maxMembers }) }); if (!response?.groupId) { throw new Error('创建群组失败: 未返回群组ID'); } try { await groupManager.joinGroup(response.groupId); showToast('success', '课堂创建并加入成功'); showMessage(`课堂 ${className} 创建成功,ID: ${response.groupId}`); setClassId(response.groupId); setIsJoinedClass(true); const messageManager = imEngine.current.getMessageManager(); if (messageManager) { imMessageManager.current = messageManager; listenMessageEvents(); } await joinRtcChannel(response.groupId); // const baseUrl = window.location.href.split('?')[0].replace(/\/[^/]*$/, ''); // setShareLink(`${baseUrl}/mobile/classroom/${response.groupId}/student`); buildShareLink(response.groupId) return response.groupId; } catch (joinErr: any) { throw new Error(`创建成功但加入失败: ${joinErr.message}`); } } catch (err: any) { const errorMsg = err.message.includes('alreadyExist') ? '课堂已存在' : `课堂创建失败: ${err.message}`; setErrorMessage(errorMsg); showToast('error', errorMsg); return null; } }; const joinRtcChannel = async (classId: string, publishOptions?: { publishVideo?: boolean publishAudio?: boolean publishScreen?: boolean }) => { if (!aliRtcEngine.current) return; const { publishVideo = false, publishAudio = false, publishScreen = false, } = publishOptions || {}; const {appId, token, timestamp} = await ClassroomAPI.getRTCToken(classId, userId); await aliRtcEngine.current.publishLocalVideoStream(publishVideo); await aliRtcEngine.current.publishLocalAudioStream(publishAudio); await aliRtcEngine.current.publishLocalScreenShareStream(publishScreen); await aliRtcEngine.current.joinChannel( { channelId: classId, userId, appId, token, timestamp, }, userId ); }; const leaveRtcChannel = async () => { if (!aliRtcEngine.current) return; await aliRtcEngine.current.leaveChannel(); }; // 切换摄像头状态 const toggleCamera = async () => { if(!aliRtcEngine.current?.isInCall){ showToast('error', '先加入课堂'); return; } try { if (isCameraOn) { await aliRtcEngine.current?.stopPreview(); await aliRtcEngine.current?.enableLocalVideo(false) await aliRtcEngine.current?.publishLocalVideoStream(false) } else { await aliRtcEngine.current?.setLocalViewConfig('localPreviewer', AliRtcVideoTrack.AliRtcVideoTrackCamera); await aliRtcEngine.current?.enableLocalVideo(true) await aliRtcEngine.current?.startPreview(); await aliRtcEngine.current?.publishLocalVideoStream(true) } await aliRtcEngine.current?.startAndPublishDefaultDevices() setIsCameraOn(!isCameraOn); } catch (err) { console.error('切换摄像头状态失败:', err); showToast('error', '切换摄像头失败'); } }; // 切换音频状态 const toggleAudio = async () => { if(!aliRtcEngine.current?.isInCall){ showToast('error', '先加入课堂'); return; } try { if (isAudioOn) { await aliRtcEngine.current?.stopAudioCapture() await aliRtcEngine.current?.publishLocalAudioStream(false); } else { await aliRtcEngine.current?.publishLocalAudioStream(true); } await aliRtcEngine.current?.startAndPublishDefaultDevices(); setIsAudioOn(!isAudioOn); } catch (err) { console.error('切换麦克风状态失败:', err); showToast('error', '切换麦克风失败'); } }; // 切换屏幕分享状态 const toggleScreenShare = async () => { if(!aliRtcEngine.current?.isInCall){ showToast('error', '先加入课堂'); return; } try { if (isScreenSharing) { await aliRtcEngine.current?.publishLocalScreenShareStream(false) await aliRtcEngine.current?.stopScreenShare() } else { await aliRtcEngine.current?.publishLocalScreenShareStream(true) await aliRtcEngine.current?.setLocalViewConfig( 'screenPreviewer', AliRtcVideoTrack.AliRtcVideoTrackScreen ); } await aliRtcEngine.current?.startAndPublishDefaultDevices() setIsScreenSharing(!isScreenSharing); } catch (err) { console.error('切换屏幕分享失败:', err); showToast('error', '切换屏幕分享失败'); } }; const handUp = async (question?: string): Promise => { if (!imMessageManager.current || !classId || role !== 'student') return; try { await imMessageManager.current.sendGroupMessage({ groupId: classId, data: JSON.stringify({ action: 'hand_up', studentId: userId, timestamp: Date.now(), question }), type: 88891, level: NORMAL, }); } catch (err: any) { setErrorMessage(`举手失败: ${err.message}`); } }; const muteStudent = async (studentId: string): Promise => { if (!imMessageManager.current || !classId || role !== Role.Teacher) return; try { await imMessageManager.current.sendGroupMessage({ groupId: classId, data: JSON.stringify({ action: 'toggle_mute', userId: studentId, mute: true }), type: 88890, level: HIGH, }); showToast('info', `已静音学生 ${studentId}`); } catch (err: any) { setErrorMessage(`静音失败: ${err.message}`); } }; const kickStudent = async (studentId: string): Promise => { if (!imGroupManager.current || !classId || role !== Role.Teacher) return; try { await imGroupManager.current.leaveGroup(classId); showToast('info', `已移出学生 ${studentId}`); } catch (err: any) { setErrorMessage(`移出失败: ${err.message}`); } }; const answerHandUp = async (studentId: string): Promise => { if (!imMessageManager.current || !classId || role !== Role.Teacher) return; try { await imMessageManager.current.sendGroupMessage({ groupId: classId, data: JSON.stringify({ action: 'answer_hand_up', studentId }), type: 88893, level: HIGH, }); showToast('info', `已应答学生 ${studentId} 的举手`); } catch (err: any) { setErrorMessage(`应答失败: ${err.message}`); } }; const sendQuestion = async (question: string): Promise => { if (!imMessageManager.current || !classId) return; try { await imMessageManager.current.sendGroupMessage({ groupId: classId, data: question, type: 88892, level: NORMAL, }); } catch (err: any) { setErrorMessage(`问题发送失败: ${err.message}`); } }; // 清理资源 useEffect(() => { return () => { if (imGroupManager.current) { imGroupManager.current.removeAllListeners(); } if (imMessageManager.current) { imMessageManager.current.removeAllListeners(); } if (imEngine.current) { imEngine.current.removeAllListeners(); } if (aliRtcEngine.current) { aliRtcEngine.current.destroy(); } }; }, []); return { // 状态 userId, isCameraOn, isAudioOn, isScreenSharing, className, setClassName, role, setRole, classId, setClassId, isLoggedIn, isJoinedClass, msgText, setMsgText, messageList, errorMessage, classStatus, handUpList, questions, students, shareLink, remoteScreenContainer, // 重命名为remoteScreenContainer remoteCameraContainer, // 导出摄像头容器ref showCameraOverlay, setShowCameraOverlay, // 方法 login, joinClass, leaveClass, sendMessage, startClass, endClass, toggleMuteMember, createClass, toggleCamera, toggleAudio, toggleScreenShare, handUp, answerHandUp, sendQuestion, muteStudent, kickStudent }; };