|
@@ -0,0 +1,920 @@
|
|
|
|
|
+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<typeof AliVCInteraction.ImEngine>;
|
|
|
|
|
+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<string>('');
|
|
|
|
|
+ const [isCameraOn, setIsCameraOn] = useState<boolean>(false);
|
|
|
|
|
+ const [isAudioOn, setIsAudioOn] = useState<boolean>(false);
|
|
|
|
|
+ const [isScreenSharing, setIsScreenSharing] = useState<boolean>(false);
|
|
|
|
|
+ const [className, setClassName] = useState<string>('');
|
|
|
|
|
+ const [role, setRole] = useState<Role | undefined>();
|
|
|
|
|
+ const [classId, setClassId] = useState<string>('');
|
|
|
|
|
+ const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
|
|
|
|
|
+ const [isJoinedClass, setIsJoinedClass] = useState<boolean>(false);
|
|
|
|
|
+ const [msgText, setMsgText] = useState<string>('');
|
|
|
|
|
+ const [messageList, setMessageList] = useState<string[]>([]);
|
|
|
|
|
+ const [errorMessage, setErrorMessage] = useState<string>('');
|
|
|
|
|
+ const [classStatus, setClassStatus] = useState<ClassStatus>(ClassStatus.NOT_STARTED);
|
|
|
|
|
+ const [handUpList, setHandUpList] = useState<HandUpRequest[]>([]);
|
|
|
|
|
+ const [questions, setQuestions] = useState<Question[]>([]);
|
|
|
|
|
+ const [students, setStudents] = useState<Array<{id: string, name: string}>>([]);
|
|
|
|
|
+ const [shareLink, setShareLink] = useState<string>('');
|
|
|
|
|
+
|
|
|
|
|
+ // SDK实例
|
|
|
|
|
+ const imEngine = useRef<ImEngine | null>(null);
|
|
|
|
|
+ const imGroupManager = useRef<ImGroupManager | null>(null);
|
|
|
|
|
+ const imMessageManager = useRef<ImMessageManager | null>(null);
|
|
|
|
|
+ const aliRtcEngine = useRef<AliRtcEngine | null>(null);
|
|
|
|
|
+ const remoteVideoElMap = useRef<Record<string, HTMLVideoElement>>({});
|
|
|
|
|
+ const remoteVideoContainer = useRef<HTMLDivElement>(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<string> => {
|
|
|
|
|
+ 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<string> => {
|
|
|
|
|
+ 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<void> => {
|
|
|
|
|
+ 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<void> => {
|
|
|
|
|
+ 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<void> => {
|
|
|
|
|
+ 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<void> => {
|
|
|
|
|
+ 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<void> => {
|
|
|
|
|
+ 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<void> => {
|
|
|
|
|
+ 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<void> => {
|
|
|
|
|
+ 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<string | null> => {
|
|
|
|
|
+ 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<void> => {
|
|
|
|
|
+ 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<void> => {
|
|
|
|
|
+ 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<void> => {
|
|
|
|
|
+ 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<void> => {
|
|
|
|
|
+ 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<void> => {
|
|
|
|
|
+ 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
|
|
|
|
|
+ };
|
|
|
|
|
+};
|
|
|
|
|
+
|