2
0

useClassroom.ts 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922
  1. import { useState, useEffect, useRef } from 'react';
  2. import { useParams } from 'react-router';
  3. import { User } from '../../../share/types.ts';
  4. import { ClassroomAPI } from '../../api/index.ts';
  5. // @ts-types="../../../share/aliyun-rtc-sdk.d.ts"
  6. import AliRtcEngine, { AliRtcSubscribeState, AliRtcVideoTrack } from 'aliyun-rtc-sdk';
  7. import { toast } from 'react-toastify';
  8. export enum Role {
  9. Teacher = 'admin',
  10. Student = 'student'
  11. }
  12. // 从SDK中提取需要的类型和枚举
  13. type ImEngine = InstanceType<typeof AliVCInteraction.ImEngine>;
  14. type ImGroupManager = AliVCInteraction.AliVCIMGroupManager;
  15. type ImMessageManager = AliVCInteraction.AliVCIMMessageManager;
  16. type ImLogLevel = AliVCInteraction.ImLogLevel;
  17. type ImMessageLevel = AliVCInteraction.ImMessageLevel;
  18. const { ERROR } = AliVCInteraction.ImLogLevel;
  19. const { NORMAL, HIGH } = AliVCInteraction.ImMessageLevel;
  20. interface ImUser {
  21. userId: string;
  22. userExtension?: string;
  23. }
  24. interface ImGroupMessage {
  25. groupId: string;
  26. type: number;
  27. data: string;
  28. sender?: ImUser;
  29. timestamp?: number;
  30. }
  31. // 互动消息类型
  32. enum InteractionAction {
  33. HandUp = 'hand_up',
  34. CancelHandUp = 'cancel_hand_up',
  35. AnswerHandUp = 'answer_hand_up'
  36. }
  37. interface InteractionMessage {
  38. action: InteractionAction;
  39. studentId: string;
  40. studentName?: string;
  41. timestamp?: number;
  42. question?: string;
  43. }
  44. interface HandUpRequest {
  45. studentId: string;
  46. studentName?: string;
  47. timestamp: number;
  48. question?: string;
  49. }
  50. interface Question {
  51. studentId: string;
  52. studentName?: string;
  53. question: string;
  54. timestamp: number;
  55. }
  56. export enum ClassStatus {
  57. NOT_STARTED = 'not_started',
  58. IN_PROGRESS = 'in_progress',
  59. ENDED = 'ended'
  60. }
  61. export const useClassroom = ({ user }:{ user : User }) => {
  62. // 状态管理
  63. // const [userId, setUserId] = useState<string>(''); // 保持string类型
  64. const userId = user.id.toString();
  65. const [isCameraOn, setIsCameraOn] = useState<boolean>(false);
  66. const [isAudioOn, setIsAudioOn] = useState<boolean>(false);
  67. const [isScreenSharing, setIsScreenSharing] = useState<boolean>(false);
  68. const [className, setClassName] = useState<string>('');
  69. const [role, setRole] = useState<Role | undefined>();
  70. const [classId, setClassId] = useState<string>('');
  71. const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
  72. const [isJoinedClass, setIsJoinedClass] = useState<boolean>(false);
  73. const [msgText, setMsgText] = useState<string>('');
  74. const [messageList, setMessageList] = useState<string[]>([]);
  75. const [errorMessage, setErrorMessage] = useState<string>('');
  76. const [classStatus, setClassStatus] = useState<ClassStatus>(ClassStatus.NOT_STARTED);
  77. const [handUpList, setHandUpList] = useState<HandUpRequest[]>([]);
  78. const [questions, setQuestions] = useState<Question[]>([]);
  79. const [students, setStudents] = useState<Array<{id: string, name: string}>>([]);
  80. const [shareLink, setShareLink] = useState<string>('');
  81. const [showCameraOverlay, setShowCameraOverlay] = useState<boolean>(true);
  82. // SDK实例
  83. const imEngine = useRef<ImEngine | null>(null);
  84. const imGroupManager = useRef<ImGroupManager | null>(null);
  85. const imMessageManager = useRef<ImMessageManager | null>(null);
  86. const aliRtcEngine = useRef<AliRtcEngine | null>(null);
  87. const remoteVideoElMap = useRef<Record<string, HTMLVideoElement>>({});
  88. const remoteScreenContainer = useRef<HTMLDivElement>(null); // 主屏幕共享容器(重命名)
  89. const remoteCameraContainer = useRef<HTMLDivElement>(null); // 摄像头小窗容器
  90. // 辅助函数
  91. const showMessage = (text: string): void => {
  92. setMessageList((prevMessageList) => [...prevMessageList, text])
  93. };
  94. const showToast = (type: 'info' | 'success' | 'error', message: string): void => {
  95. toast[type](message);
  96. };
  97. // 事件监听函数
  98. const listenImEvents = (): void => {
  99. if (!imEngine.current) return;
  100. if (!role) return;
  101. imEngine.current.on('connectsuccess', () => {
  102. showMessage('IM连接成功');
  103. });
  104. imEngine.current.on('disconnect', async (code: number) => {
  105. showMessage(`IM断开连接: ${code}`);
  106. // 自动重连
  107. try {
  108. const { token, nonce, timestamp } = await ClassroomAPI.getIMToken(userId, role);
  109. await imEngine.current!.login({
  110. user: {
  111. userId,
  112. userExtension: JSON.stringify(user)
  113. },
  114. userAuth: {
  115. nonce,
  116. timestamp,
  117. token,
  118. role
  119. }
  120. });
  121. showMessage('IM自动重连成功');
  122. } catch (err: unknown) {
  123. const error = err as Error;
  124. showMessage(`IM自动重连失败: ${error.message}`);
  125. }
  126. });
  127. };
  128. const listenGroupEvents = (): void => {
  129. if (!imGroupManager.current) return;
  130. imGroupManager.current.on('memberchange', (groupId: string, memberCount: number, joinUsers: ImUser[], leaveUsers: ImUser[]) => {
  131. showMessage(`成员变更: 加入${joinUsers.length}人, 离开${leaveUsers.length}人`);
  132. });
  133. };
  134. const listenMessageEvents = (): void => {
  135. if (!imMessageManager.current) return;
  136. imMessageManager.current.on('recvgroupmessage', (msg: AliVCInteraction.ImMessage, groupId: string) => {
  137. if (msg.type === 88889) { // 课堂状态消息
  138. try {
  139. const data = JSON.parse(msg.data);
  140. if (data.action === 'start_class') {
  141. setClassStatus(ClassStatus.IN_PROGRESS);
  142. showMessage('老师已开始上课');
  143. } else if (data.action === 'end_class') {
  144. setClassStatus(ClassStatus.ENDED);
  145. showMessage('老师已结束上课');
  146. }
  147. } catch (err) {
  148. console.error('解析课堂状态消息失败', err);
  149. }
  150. } else if (msg.type === 88890) { // 静音指令
  151. try {
  152. const data = JSON.parse(msg.data);
  153. if (data.action === 'toggle_mute' && data.userId === userId) {
  154. showMessage(data.mute ? '你已被老师静音' : '老师已取消你的静音');
  155. }
  156. } catch (err) {
  157. console.error('解析静音指令失败', err);
  158. }
  159. } else if (msg.type === 88891) { // 举手消息
  160. try {
  161. const data = JSON.parse(msg.data) as InteractionMessage;
  162. if (data.action === InteractionAction.HandUp) {
  163. const handUpData: HandUpRequest = {
  164. ...data,
  165. timestamp: data.timestamp || Date.now()
  166. };
  167. setHandUpList([...handUpList, handUpData]);
  168. showMessage(`${data.studentName || data.studentId} 举手了`);
  169. } else if (data.action === InteractionAction.CancelHandUp) {
  170. setHandUpList(handUpList.filter(h => h.studentId !== data.studentId));
  171. }
  172. } catch (err) {
  173. console.error('解析举手消息失败', err);
  174. }
  175. } else if (msg.type === 88892) { // 问题消息
  176. try {
  177. const data = JSON.parse(msg.data) as {question: string};
  178. if (typeof data.question === 'string') {
  179. const question: Question = {
  180. studentId: msg.sender?.userId || 'unknown',
  181. studentName: (() => {
  182. try {
  183. return msg.sender?.userExtension ? JSON.parse(msg.sender.userExtension)?.nickname : null;
  184. } catch {
  185. return null;
  186. }
  187. })() || msg.sender?.userId || '未知用户',
  188. question: data.question,
  189. timestamp: msg.timestamp || Date.now()
  190. };
  191. setQuestions([...questions, question]);
  192. }
  193. showMessage(`收到问题: ${data.question}`);
  194. } catch (err) {
  195. console.error('解析问题消息失败', err);
  196. }
  197. } else if (msg.type === 88893) { // 应答消息
  198. try {
  199. const data = JSON.parse(msg.data) as InteractionMessage;
  200. if (data.action === InteractionAction.AnswerHandUp && data.studentId === userId) {
  201. showMessage('老师已应答你的举手');
  202. setHandUpList(handUpList.filter(h => h.studentId !== data.studentId));
  203. }
  204. } catch (err) {
  205. console.error('解析应答消息失败', err);
  206. }
  207. } else if (msg.type === 88888) { // 普通文本消息
  208. const sender = msg.sender;
  209. const userExtension = JSON.parse(sender?.userExtension || '{}') as User;
  210. const senderName = userExtension.nickname || userExtension.username;
  211. showMessage(`${ senderName || '未知用户' }: ${msg.data}`);
  212. }
  213. });
  214. };
  215. // RTC相关函数
  216. const removeRemoteVideo = (userId: string, type: 'camera' | 'screen' = 'camera') => {
  217. const vid = `${type}_${userId}`;
  218. const el = remoteVideoElMap.current[vid];
  219. if (el) {
  220. aliRtcEngine.current!.setRemoteViewConfig(null, userId, type === 'camera' ? AliRtcVideoTrack.AliRtcVideoTrackCamera : AliRtcVideoTrack.AliRtcVideoTrackScreen);
  221. el.pause();
  222. // 根据流类型从不同容器移除
  223. if (type === 'camera') {
  224. remoteCameraContainer.current?.removeChild(el);
  225. } else {
  226. remoteScreenContainer.current?.removeChild(el);
  227. }
  228. delete remoteVideoElMap.current[vid];
  229. }
  230. };
  231. const listenRtcEvents = () => {
  232. if (!aliRtcEngine.current) return;
  233. showMessage('注册rtc事件监听')
  234. aliRtcEngine.current.on('remoteUserOnLineNotify', (userId: string) => {
  235. showMessage(`用户 ${userId} 加入课堂`);
  236. console.log('用户上线通知:', userId);
  237. });
  238. aliRtcEngine.current.on('remoteUserOffLineNotify', (userId: string) => {
  239. showMessage(`用户 ${userId} 离开课堂`);
  240. console.log('用户下线通知:', userId);
  241. removeRemoteVideo(userId, 'camera');
  242. removeRemoteVideo(userId, 'screen');
  243. });
  244. aliRtcEngine.current.on('videoSubscribeStateChanged', (
  245. userId: string,
  246. oldState: AliRtcSubscribeState,
  247. newState: AliRtcSubscribeState,
  248. interval: number,
  249. channelId: string
  250. ) => {
  251. console.log(`视频订阅状态变化: 用户 ${userId}, 旧状态 ${oldState}, 新状态 ${newState}`);
  252. switch(newState) {
  253. case 3: // 订阅成功
  254. try {
  255. console.log('开始创建远程视频元素');
  256. if (remoteVideoElMap.current[`camera_${userId}`]) {
  257. console.log(`用户 ${userId} 的视频元素已存在`);
  258. return;
  259. }
  260. const video = document.createElement('video');
  261. video.autoplay = true;
  262. video.playsInline = true;
  263. video.className = 'w-80 h-45 mr-2 mb-2 bg-black';
  264. if (!remoteCameraContainer.current) {
  265. console.error('摄像头视频容器未找到');
  266. return;
  267. }
  268. remoteCameraContainer.current.appendChild(video);
  269. remoteVideoElMap.current[`camera_${userId}`] = video;
  270. aliRtcEngine.current!.setRemoteViewConfig(
  271. video,
  272. userId,
  273. AliRtcVideoTrack.AliRtcVideoTrackCamera
  274. );
  275. console.log(`已订阅用户 ${userId} 的视频流`);
  276. showMessage(`已显示用户 ${userId} 的视频`);
  277. } catch (err) {
  278. console.error(`订阅用户 ${userId} 视频流失败:`, err);
  279. showMessage(`订阅用户 ${userId} 视频流失败`);
  280. }
  281. break;
  282. case 1: // 取消订阅
  283. console.log(`取消订阅用户 ${userId} 的视频流`);
  284. showMessage(`取消订阅用户 ${userId} 的视频流`);
  285. removeRemoteVideo(userId, 'camera');
  286. break;
  287. case 2: // 订阅中
  288. console.log(`正在订阅用户 ${userId} 的视频流...`);
  289. break;
  290. default:
  291. console.warn(`未知订阅状态: ${newState}`);
  292. }
  293. });
  294. aliRtcEngine.current.on('screenShareSubscribeStateChanged', (
  295. userId: string,
  296. oldState: AliRtcSubscribeState,
  297. newState: AliRtcSubscribeState,
  298. elapseSinceLastState: number,
  299. channel: string
  300. ) => {
  301. console.log(`屏幕分享订阅状态变更:uid=${userId}, oldState=${oldState}, newState=${newState}`);
  302. switch(newState) {
  303. case 3: // 订阅成功
  304. try {
  305. console.log('开始创建屏幕分享视频元素');
  306. if (remoteVideoElMap.current[`screen_${userId}`]) {
  307. console.log(`用户 ${userId} 的屏幕分享元素已存在`);
  308. return;
  309. }
  310. const video = document.createElement('video');
  311. video.autoplay = true;
  312. video.playsInline = true;
  313. video.className = 'w-full h-full bg-black';
  314. if (!remoteScreenContainer.current) {
  315. console.error('屏幕共享容器未找到');
  316. return;
  317. }
  318. remoteScreenContainer.current.appendChild(video);
  319. remoteVideoElMap.current[`screen_${userId}`] = video;
  320. aliRtcEngine.current!.setRemoteViewConfig(
  321. video,
  322. userId,
  323. AliRtcVideoTrack.AliRtcVideoTrackScreen
  324. );
  325. console.log(`已订阅用户 ${userId} 的屏幕分享流`);
  326. showMessage(`已显示用户 ${userId} 的屏幕分享`);
  327. } catch (err) {
  328. console.error(`订阅用户 ${userId} 屏幕分享流失败:`, err);
  329. showMessage(`订阅用户 ${userId} 屏幕分享流失败`);
  330. }
  331. break;
  332. case 1: // 取消订阅
  333. console.log(`取消订阅用户 ${userId} 的屏幕分享流`);
  334. showMessage(`取消订阅用户 ${userId} 的屏幕分享流`);
  335. removeRemoteVideo(userId, 'screen');
  336. break;
  337. case 2: // 订阅中
  338. console.log(`正在订阅用户 ${userId} 的屏幕分享流...`);
  339. break;
  340. default:
  341. console.warn(`未知屏幕分享订阅状态: ${newState}`);
  342. }
  343. });
  344. };
  345. // 课堂操作方法
  346. const login = async (role: Role): Promise<void> => {
  347. if(!role) {
  348. showToast('error', '角色不存在');
  349. return;
  350. }
  351. try {
  352. const { ImEngine: ImEngineClass } = window.AliVCInteraction;
  353. const {appId, appSign, timestamp, nonce, token} = await ClassroomAPI.getIMToken(userId, role);
  354. imEngine.current = ImEngineClass.createEngine();
  355. await imEngine.current.init({
  356. deviceId: 'xxxx',
  357. appId,
  358. appSign,
  359. logLevel: ERROR,
  360. });
  361. await imEngine.current.login({
  362. user: {
  363. userId,
  364. userExtension: JSON.stringify({ nickname: user?.nickname || user?.username || '' })
  365. },
  366. userAuth: {
  367. nonce,
  368. timestamp,
  369. token,
  370. role
  371. }
  372. });
  373. aliRtcEngine.current = AliRtcEngine.getInstance();
  374. AliRtcEngine.setLogLevel(0);
  375. listenImEvents();
  376. listenRtcEvents();
  377. setIsLoggedIn(true);
  378. setErrorMessage('');
  379. showToast('success', '登录成功');
  380. } catch (err: any) {
  381. setErrorMessage(`登录失败: ${err.message}`);
  382. showToast('error', '登录失败');
  383. }
  384. };
  385. const joinClass = async (classId: string): Promise<void> => {
  386. if (!imEngine.current || !aliRtcEngine.current) return;
  387. // // 优先使用URL参数中的classId和role
  388. // const { id: pathClassId, role: pathRole } = useParams();
  389. // const finalClassId = (classId || pathClassId) as string;
  390. // if (pathRole && ['teacher', 'student'].includes(pathRole)) {
  391. // setRole(pathRole === 'teacher' ? Role.Teacher : Role.Student);
  392. // }
  393. // if (!finalClassId) {
  394. // setErrorMessage('课堂ID不能为空');
  395. // showToast('error', '请输入有效的课堂ID');
  396. // return;
  397. // }
  398. try {
  399. const gm = imEngine.current.getGroupManager();
  400. const mm = imEngine.current.getMessageManager();
  401. imGroupManager.current = gm || null;
  402. imMessageManager.current = mm || null;
  403. await gm!.joinGroup(classId);
  404. listenGroupEvents();
  405. listenMessageEvents();
  406. await joinRtcChannel(classId);
  407. buildShareLink(classId)
  408. setIsJoinedClass(true);
  409. setErrorMessage('');
  410. showToast('success', '加入课堂成功');
  411. } catch (err: any) {
  412. setErrorMessage(`加入课堂失败: ${err.message}`);
  413. showToast('error', '加入课堂失败');
  414. if (imGroupManager.current) {
  415. try {
  416. await imGroupManager.current.leaveGroup(classId);
  417. } catch (leaveErr) {
  418. console.error('离开IM群组失败:', leaveErr);
  419. }
  420. }
  421. }
  422. };
  423. const leaveClass = async (): Promise<void> => {
  424. try {
  425. if (imGroupManager.current && classId) {
  426. await imGroupManager.current.leaveGroup(classId);
  427. }
  428. if (aliRtcEngine.current) {
  429. await leaveRtcChannel();
  430. }
  431. setIsJoinedClass(false);
  432. showToast('info', '已离开课堂');
  433. } catch (err) {
  434. console.error('离开课堂失败:', err);
  435. showToast('error', '离开课堂时发生错误');
  436. }
  437. };
  438. const sendMessage = async (): Promise<void> => {
  439. if (!imMessageManager.current || !classId) return;
  440. if (!msgText.trim()) {
  441. showToast('error', '消息不能为空');
  442. return;
  443. }
  444. try {
  445. await imMessageManager.current.sendGroupMessage({
  446. groupId: classId,
  447. data: msgText,
  448. type: 88888,
  449. level: NORMAL,
  450. });
  451. setMsgText('');
  452. setErrorMessage('');
  453. } catch (err: any) {
  454. setErrorMessage(`消息发送失败: ${err.message}`);
  455. }
  456. };
  457. const startClass = async (): Promise<void> => {
  458. if (!imMessageManager.current || !classId || role !== Role.Teacher) return;
  459. try {
  460. await imMessageManager.current.sendGroupMessage({
  461. groupId: classId,
  462. data: JSON.stringify({ action: 'start_class' }),
  463. type: 88889,
  464. level: HIGH,
  465. });
  466. setClassStatus(ClassStatus.IN_PROGRESS);
  467. showToast('success', '课堂已开始');
  468. } catch (err: any) {
  469. setErrorMessage(`开始上课失败: ${err.message}`);
  470. }
  471. };
  472. const endClass = async (): Promise<void> => {
  473. if (!imMessageManager.current || !classId || role !== Role.Teacher) return;
  474. try {
  475. await imMessageManager.current.sendGroupMessage({
  476. groupId: classId,
  477. data: JSON.stringify({ action: 'end_class' }),
  478. type: 88889,
  479. level: HIGH,
  480. });
  481. setClassStatus(ClassStatus.ENDED);
  482. showToast('success', '课堂已结束');
  483. try {
  484. await leaveRtcChannel();
  485. } catch (err: any) {
  486. console.error('离开RTC频道失败:', err);
  487. showToast('error', '离开RTC频道失败');
  488. }
  489. } catch (err: any) {
  490. setErrorMessage(`结束上课失败: ${err.message}`);
  491. }
  492. };
  493. const toggleMuteMember = async (userId: string, mute: boolean): Promise<void> => {
  494. if (!imMessageManager.current || !classId || role !== Role.Teacher) return;
  495. try {
  496. await imMessageManager.current.sendGroupMessage({
  497. groupId: classId,
  498. data: JSON.stringify({
  499. action: 'toggle_mute',
  500. userId,
  501. mute
  502. }),
  503. type: 88890,
  504. level: HIGH,
  505. });
  506. showToast('info', mute ? `已静音用户 ${userId}` : `已取消静音用户 ${userId}`);
  507. } catch (err: any) {
  508. setErrorMessage(`操作失败: ${err.message}`);
  509. }
  510. };
  511. const buildShareLink = (classId: string) => {
  512. const getBaseUrl = () => {
  513. const protocol = window.location.protocol;
  514. const host = window.location.host;
  515. return `${protocol}//${host}`;
  516. }
  517. // const baseUrl = window.location.href.split('?')[0].replace(/\/[^/]*$/, '');
  518. const baseUrl = getBaseUrl();
  519. setShareLink(`${baseUrl}/mobile/classroom/${classId}/student`);
  520. }
  521. const createClass = async (className: string, maxMembers = 200): Promise<string | null> => {
  522. if (!imEngine.current || !isLoggedIn || role !== Role.Teacher) {
  523. showToast('error', '只有老师可以创建课堂');
  524. return null;
  525. }
  526. try {
  527. const groupManager = imEngine.current.getGroupManager();
  528. if (!groupManager) {
  529. throw new Error('群组管理器未初始化');
  530. }
  531. showToast('info', '正在创建课堂...');
  532. const response = await groupManager.createGroup({
  533. groupName: className,
  534. groupMeta: JSON.stringify({
  535. classType: 'interactive',
  536. creator: userId,
  537. createdAt: Date.now(),
  538. maxMembers
  539. })
  540. });
  541. if (!response?.groupId) {
  542. throw new Error('创建群组失败: 未返回群组ID');
  543. }
  544. try {
  545. await groupManager.joinGroup(response.groupId);
  546. showToast('success', '课堂创建并加入成功');
  547. showMessage(`课堂 ${className} 创建成功,ID: ${response.groupId}`);
  548. setClassId(response.groupId);
  549. setIsJoinedClass(true);
  550. const messageManager = imEngine.current.getMessageManager();
  551. if (messageManager) {
  552. imMessageManager.current = messageManager;
  553. listenMessageEvents();
  554. }
  555. await joinRtcChannel(response.groupId);
  556. // const baseUrl = window.location.href.split('?')[0].replace(/\/[^/]*$/, '');
  557. // setShareLink(`${baseUrl}/mobile/classroom/${response.groupId}/student`);
  558. buildShareLink(response.groupId)
  559. return response.groupId;
  560. } catch (joinErr: any) {
  561. throw new Error(`创建成功但加入失败: ${joinErr.message}`);
  562. }
  563. } catch (err: any) {
  564. const errorMsg = err.message.includes('alreadyExist')
  565. ? '课堂已存在'
  566. : `课堂创建失败: ${err.message}`;
  567. setErrorMessage(errorMsg);
  568. showToast('error', errorMsg);
  569. return null;
  570. }
  571. };
  572. const joinRtcChannel = async (classId: string, publishOptions?: {
  573. publishVideo?: boolean
  574. publishAudio?: boolean
  575. publishScreen?: boolean
  576. }) => {
  577. if (!aliRtcEngine.current) return;
  578. const {
  579. publishVideo = false,
  580. publishAudio = false,
  581. publishScreen = false,
  582. } = publishOptions || {};
  583. const {appId, token, timestamp} = await ClassroomAPI.getRTCToken(classId, userId);
  584. await aliRtcEngine.current.publishLocalVideoStream(publishVideo);
  585. await aliRtcEngine.current.publishLocalAudioStream(publishAudio);
  586. await aliRtcEngine.current.publishLocalScreenShareStream(publishScreen);
  587. await aliRtcEngine.current.joinChannel(
  588. {
  589. channelId: classId,
  590. userId,
  591. appId,
  592. token,
  593. timestamp,
  594. },
  595. userId
  596. );
  597. };
  598. const leaveRtcChannel = async () => {
  599. if (!aliRtcEngine.current) return;
  600. await aliRtcEngine.current.leaveChannel();
  601. };
  602. // 切换摄像头状态
  603. const toggleCamera = async () => {
  604. if(!aliRtcEngine.current?.isInCall){
  605. showToast('error', '先加入课堂');
  606. return;
  607. }
  608. try {
  609. if (isCameraOn) {
  610. await aliRtcEngine.current?.stopPreview();
  611. await aliRtcEngine.current?.enableLocalVideo(false)
  612. await aliRtcEngine.current?.publishLocalVideoStream(false)
  613. } else {
  614. await aliRtcEngine.current?.setLocalViewConfig('localPreviewer', AliRtcVideoTrack.AliRtcVideoTrackCamera);
  615. await aliRtcEngine.current?.enableLocalVideo(true)
  616. await aliRtcEngine.current?.startPreview();
  617. await aliRtcEngine.current?.publishLocalVideoStream(true)
  618. }
  619. await aliRtcEngine.current?.startAndPublishDefaultDevices()
  620. setIsCameraOn(!isCameraOn);
  621. } catch (err) {
  622. console.error('切换摄像头状态失败:', err);
  623. showToast('error', '切换摄像头失败');
  624. }
  625. };
  626. // 切换音频状态
  627. const toggleAudio = async () => {
  628. if(!aliRtcEngine.current?.isInCall){
  629. showToast('error', '先加入课堂');
  630. return;
  631. }
  632. try {
  633. if (isAudioOn) {
  634. await aliRtcEngine.current?.stopAudioCapture()
  635. await aliRtcEngine.current?.publishLocalAudioStream(false);
  636. } else {
  637. await aliRtcEngine.current?.publishLocalAudioStream(true);
  638. }
  639. await aliRtcEngine.current?.startAndPublishDefaultDevices();
  640. setIsAudioOn(!isAudioOn);
  641. } catch (err) {
  642. console.error('切换麦克风状态失败:', err);
  643. showToast('error', '切换麦克风失败');
  644. }
  645. };
  646. // 切换屏幕分享状态
  647. const toggleScreenShare = async () => {
  648. if(!aliRtcEngine.current?.isInCall){
  649. showToast('error', '先加入课堂');
  650. return;
  651. }
  652. try {
  653. if (isScreenSharing) {
  654. await aliRtcEngine.current?.publishLocalScreenShareStream(false)
  655. await aliRtcEngine.current?.stopScreenShare()
  656. } else {
  657. await aliRtcEngine.current?.publishLocalScreenShareStream(true)
  658. await aliRtcEngine.current?.setLocalViewConfig(
  659. 'screenPreviewer',
  660. AliRtcVideoTrack.AliRtcVideoTrackScreen
  661. );
  662. }
  663. await aliRtcEngine.current?.startAndPublishDefaultDevices()
  664. setIsScreenSharing(!isScreenSharing);
  665. } catch (err) {
  666. console.error('切换屏幕分享失败:', err);
  667. showToast('error', '切换屏幕分享失败');
  668. }
  669. };
  670. const handUp = async (question?: string): Promise<void> => {
  671. if (!imMessageManager.current || !classId || role !== 'student') return;
  672. try {
  673. await imMessageManager.current.sendGroupMessage({
  674. groupId: classId,
  675. data: JSON.stringify({
  676. action: 'hand_up',
  677. studentId: userId,
  678. timestamp: Date.now(),
  679. question
  680. }),
  681. type: 88891,
  682. level: NORMAL,
  683. });
  684. } catch (err: any) {
  685. setErrorMessage(`举手失败: ${err.message}`);
  686. }
  687. };
  688. const muteStudent = async (studentId: string): Promise<void> => {
  689. if (!imMessageManager.current || !classId || role !== Role.Teacher) return;
  690. try {
  691. await imMessageManager.current.sendGroupMessage({
  692. groupId: classId,
  693. data: JSON.stringify({
  694. action: 'toggle_mute',
  695. userId: studentId,
  696. mute: true
  697. }),
  698. type: 88890,
  699. level: HIGH,
  700. });
  701. showToast('info', `已静音学生 ${studentId}`);
  702. } catch (err: any) {
  703. setErrorMessage(`静音失败: ${err.message}`);
  704. }
  705. };
  706. const kickStudent = async (studentId: string): Promise<void> => {
  707. if (!imGroupManager.current || !classId || role !== Role.Teacher) return;
  708. try {
  709. await imGroupManager.current.leaveGroup(classId);
  710. showToast('info', `已移出学生 ${studentId}`);
  711. } catch (err: any) {
  712. setErrorMessage(`移出失败: ${err.message}`);
  713. }
  714. };
  715. const answerHandUp = async (studentId: string): Promise<void> => {
  716. if (!imMessageManager.current || !classId || role !== Role.Teacher) return;
  717. try {
  718. await imMessageManager.current.sendGroupMessage({
  719. groupId: classId,
  720. data: JSON.stringify({
  721. action: 'answer_hand_up',
  722. studentId
  723. }),
  724. type: 88893,
  725. level: HIGH,
  726. });
  727. showToast('info', `已应答学生 ${studentId} 的举手`);
  728. } catch (err: any) {
  729. setErrorMessage(`应答失败: ${err.message}`);
  730. }
  731. };
  732. const sendQuestion = async (question: string): Promise<void> => {
  733. if (!imMessageManager.current || !classId) return;
  734. try {
  735. await imMessageManager.current.sendGroupMessage({
  736. groupId: classId,
  737. data: question,
  738. type: 88892,
  739. level: NORMAL,
  740. });
  741. } catch (err: any) {
  742. setErrorMessage(`问题发送失败: ${err.message}`);
  743. }
  744. };
  745. // 清理资源
  746. useEffect(() => {
  747. return () => {
  748. if (imGroupManager.current) {
  749. imGroupManager.current.removeAllListeners();
  750. }
  751. if (imMessageManager.current) {
  752. imMessageManager.current.removeAllListeners();
  753. }
  754. if (imEngine.current) {
  755. imEngine.current.removeAllListeners();
  756. }
  757. if (aliRtcEngine.current) {
  758. aliRtcEngine.current.destroy();
  759. }
  760. };
  761. }, []);
  762. return {
  763. // 状态
  764. userId,
  765. isCameraOn,
  766. isAudioOn,
  767. isScreenSharing,
  768. className,
  769. setClassName,
  770. role,
  771. setRole,
  772. classId,
  773. setClassId,
  774. isLoggedIn,
  775. isJoinedClass,
  776. msgText,
  777. setMsgText,
  778. messageList,
  779. errorMessage,
  780. classStatus,
  781. handUpList,
  782. questions,
  783. students,
  784. shareLink,
  785. remoteScreenContainer, // 重命名为remoteScreenContainer
  786. remoteCameraContainer, // 导出摄像头容器ref
  787. showCameraOverlay,
  788. setShowCameraOverlay,
  789. // 方法
  790. login,
  791. joinClass,
  792. leaveClass,
  793. sendMessage,
  794. startClass,
  795. endClass,
  796. toggleMuteMember,
  797. createClass,
  798. toggleCamera,
  799. toggleAudio,
  800. toggleScreenShare,
  801. handUp,
  802. answerHandUp,
  803. sendQuestion,
  804. muteStudent,
  805. kickStudent
  806. };
  807. };