|
@@ -1,942 +0,0 @@
|
|
|
-import { useState, useEffect, useRef } from 'react';
|
|
|
|
|
-import { useParams } from 'react-router';
|
|
|
|
|
-// import { ClassroomAPI } from '../../api/index.ts';
|
|
|
|
|
-// @ts-types="../../../share/aliyun-rtc-sdk.d.ts"
|
|
|
|
|
-// @ts-types="./alivc-im.iife.d.ts"
|
|
|
|
|
-import AliRtcEngine, { AliRtcSubscribeState, AliRtcVideoTrack } from 'aliyun-rtc-sdk';
|
|
|
|
|
-import { toast } from 'react-toastify';
|
|
|
|
|
-import { User } from '../../hooks/AuthProvider';
|
|
|
|
|
-import { aliyunClient } from '@/client/api';
|
|
|
|
|
-import { UserType } from '@/server/modules/users/user.enum';
|
|
|
|
|
-export enum Role {
|
|
|
|
|
- Teacher = UserType.TEACHER,
|
|
|
|
|
- Student = UserType.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'
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-export const useClassroom = ({ user }:{ user : User }) => {
|
|
|
|
|
- // 状态管理
|
|
|
|
|
- // const [userId, setUserId] = useState<string>(''); // 保持string类型
|
|
|
|
|
- const userId = user.id.toString();
|
|
|
|
|
- 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>('');
|
|
|
|
|
- const [showCameraOverlay, setShowCameraOverlay] = useState<boolean>(true);
|
|
|
|
|
-
|
|
|
|
|
- // 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 remoteScreenContainer = useRef<HTMLDivElement>(null); // 主屏幕共享容器(重命名)
|
|
|
|
|
- const remoteCameraContainer = 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 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 res = await aliyunClient.im_token.$post({
|
|
|
|
|
- json: { role }
|
|
|
|
|
- });
|
|
|
|
|
- if(!res.ok) {
|
|
|
|
|
- const { message } = await res.json()
|
|
|
|
|
- throw new Error(message)
|
|
|
|
|
- }
|
|
|
|
|
- const { token, nonce, timestamp } = await res.json()
|
|
|
|
|
- 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<void> => {
|
|
|
|
|
- if(!role) {
|
|
|
|
|
- showToast('error', '角色不存在');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- const loginRole = role === Role.Teacher ? 'admin' : Role.Student;
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- const { ImEngine: ImEngineClass } = window.AliVCInteraction;
|
|
|
|
|
- const res = await aliyunClient.im_token.$post({
|
|
|
|
|
- json: { role: loginRole }
|
|
|
|
|
- });
|
|
|
|
|
- if(!res.ok) {
|
|
|
|
|
- const { message } = await res.json()
|
|
|
|
|
- throw new Error(message)
|
|
|
|
|
- }
|
|
|
|
|
- const {appId, appSign, timestamp, nonce, token} = await res.json();
|
|
|
|
|
- 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: loginRole
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- 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和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<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;
|
|
|
|
|
-
|
|
|
|
|
- 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<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 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<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);
|
|
|
|
|
-
|
|
|
|
|
- // 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 res = await aliyunClient.rtc_token.$post({
|
|
|
|
|
- json: { channelId: classId }
|
|
|
|
|
- });
|
|
|
|
|
- if(!res.ok) {
|
|
|
|
|
- const { message } = await res.json()
|
|
|
|
|
- throw new Error(message)
|
|
|
|
|
- }
|
|
|
|
|
- const { appId, token, timestamp } = await res.json()
|
|
|
|
|
- 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)
|
|
|
|
|
-
|
|
|
|
|
- }
|
|
|
|
|
- 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);
|
|
|
|
|
- }
|
|
|
|
|
- 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
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- 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,
|
|
|
|
|
- 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
|
|
|
|
|
- };
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|