import { useState, useEffect, useRef } from 'react'; import { useParams } from 'react-router'; // @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' } // 配置信息 const IM_APP_ID = '4c2ab5e1b1b0'; const IM_APP_KEY = '314bb5eee5b623549e8a41574ba3ff32'; const IM_APP_SIGN = 'H4sIAAAAAAAE/wCQAG//zguHB+lYCilkv7diSkk4GmcvLuds+InRu9vFOFebMwm/jEgsK5bBT85Z0owObMxG58uXHyPFlPEBEDQm9FswNJ+KmX0VDYkcfdPPWkafA6Hc0B6F+p5De9yJfPEfHzwo/DHMaygbHfLmBgUtmKveq421sJr/gNBz9D04Ewsg39us+ao0NegzLt7xtXvFXXXJAAAA//8BAAD//yoav6aQAAAA'; const RTC_APP_ID = 'a5842c2a-d94a-43be-81de-1fdb712476e1'; const RTC_APP_KEY = 'b71d65f4f84c450f6f058f4ad507bd42'; export const useClassroom = () => { // 状态管理 const [userId, setUserId] = useState(''); 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(''); // SDK实例 const imEngine = useRef(null); const imGroupManager = useRef(null); const imMessageManager = useRef(null); const aliRtcEngine = useRef(null); const remoteVideoElMap = useRef>({}); const remoteVideoContainer = useRef(null); // 辅助函数 const showMessage = (text: string): void => { setMessageList((prevMessageList) => [...prevMessageList, text]) }; const showToast = (type: 'info' | 'success' | 'error', message: string): void => { toast[type](message); }; const hex = (buffer: ArrayBuffer): string => { const hexCodes = []; const view = new DataView(buffer); for (let i = 0; i < view.byteLength; i += 4) { const value = view.getUint32(i); const stringValue = value.toString(16); const padding = '00000000'; const paddedValue = (padding + stringValue).slice(-padding.length); hexCodes.push(paddedValue); } return hexCodes.join(''); }; const generateToken = async ( appId: string, appKey: string, channelId: string, userId: string, timestamp: number ): Promise => { const encoder = new TextEncoder(); const data = encoder.encode(`${appId}${appKey}${channelId}${userId}${timestamp}`); const hash = await crypto.subtle.digest('SHA-256', data); return hex(hash); }; const generateImToken = async (userId: string, role: string): Promise => { const nonce = 'AK_4'; const timestamp = Math.floor(Date.now() / 1000) + 3600 * 3; const pendingShaStr = `${IM_APP_ID}${IM_APP_KEY}${userId}${nonce}${timestamp}${role}`; const encoder = new TextEncoder(); const data = encoder.encode(pendingShaStr); const hash = await crypto.subtle.digest('SHA-256', data); return hex(hash); }; // 事件监听函数 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 imToken = await generateImToken(userId, role); await imEngine.current!.login({ user: { userId, userExtension: '{}' }, userAuth: { nonce: 'AK_4', timestamp: Math.floor(Date.now() / 1000) + 3600 * 3, token: imToken, 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) { // 普通文本消息 showMessage(`${msg.sender?.userId || '未知用户'}: ${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(); remoteVideoContainer.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 (!remoteVideoContainer.current) { console.error('远程视频容器未找到'); return; } remoteVideoContainer.current.style.display = 'block'; remoteVideoContainer.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} 的视频流`); 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-80 h-45 mr-2 mb-2 bg-black'; if (!remoteVideoContainer.current) { console.error('远程视频容器未找到'); return; } remoteVideoContainer.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} 的屏幕分享流`); removeRemoteVideo(userId, 'screen'); break; case 2: // 订阅中 console.log(`正在订阅用户 ${userId} 的屏幕分享流...`); break; default: console.warn(`未知屏幕分享订阅状态: ${newState}`); } }); }; // 课堂操作方法 const login = async (userId: string): Promise => { if(!role) return; try { const { ImEngine: ImEngineClass } = window.AliVCInteraction; imEngine.current = ImEngineClass.createEngine(); await imEngine.current.init({ deviceId: 'xxxx', appId: IM_APP_ID, appSign: IM_APP_SIGN, logLevel: ERROR, }); const imToken = await generateImToken(userId, role); await imEngine.current.login({ user: { userId, userExtension: '{}' }, userAuth: { nonce: 'AK_4', timestamp: Math.floor(Date.now() / 1000) + 3600 * 3, token: imToken, 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 const { id: pathClassId } = useParams(); const finalClassId = (classId || pathClassId) as string; 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(finalClassId); listenGroupEvents(); listenMessageEvents(); await joinRtcChannel(finalClassId); setIsJoinedClass(true); setErrorMessage(''); showToast('success', '加入课堂成功'); } catch (err: any) { setErrorMessage(`加入课堂失败: ${err.message}`); showToast('error', '加入课堂失败'); if (imGroupManager.current) { try { await imGroupManager.current.leaveGroup(finalClassId); } 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; 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 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); setShareLink(`${window.location.href.split('?')[0]}?classId=${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 timestamp = Math.floor(Date.now() / 1000) + 3600 * 3; const token = await generateToken(RTC_APP_ID, RTC_APP_KEY, classId, userId, timestamp); await aliRtcEngine.current.publishLocalVideoStream(publishVideo); await aliRtcEngine.current.publishLocalAudioStream(publishAudio); await aliRtcEngine.current.publishLocalScreenShareStream(publishScreen); await aliRtcEngine.current.joinChannel( { channelId: classId, userId, appId: RTC_APP_ID, 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, setUserId, isCameraOn, isAudioOn, isScreenSharing, className, setClassName, role, setRole, classId, setClassId, isLoggedIn, isJoinedClass, msgText, setMsgText, messageList, errorMessage, classStatus, handUpList, questions, students, shareLink, remoteVideoContainer, // 方法 login, joinClass, leaveClass, sendMessage, startClass, endClass, toggleMuteMember, createClass, toggleCamera, toggleAudio, toggleScreenShare, handUp, answerHandUp, sendQuestion, muteStudent, kickStudent }; };