| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- import React, { useState, useEffect, useRef, createContext, useContext } from 'react';
- import AliRtcEngine, { AliRtcSubscribeState, AliRtcVideoTrack, AliRtcSdkChannelProfile } from 'aliyun-rtc-sdk';
- import { ToastContainer, toast } from 'react-toastify';
- // 从 SDK 中提取需要的类型
- type ImEngine = InstanceType<typeof AliVCInteraction.ImEngine>;
- type ImGroupManager = AliVCInteraction.AliVCIMGroupManager;
- type ImMessageManager = AliVCInteraction.AliVCIMMessageManager;
- type ImLogLevel = AliVCInteraction.ImLogLevel;
- type ImMessageLevel = AliVCInteraction.ImMessageLevel;
- interface ImUser {
- userId: string;
- userExtension?: string;
- }
- interface ImGroupMessage {
- groupId: string;
- type: number;
- data: string;
- sender?: ImUser;
- timestamp?: number;
- }
- // 课堂上下文类型
- type ClassroomContextType = {
- userId: string;
- role: 'teacher' | 'student';
- isLoggedIn: boolean;
- isJoinedClass: boolean;
- messageList: string[];
- errorMessage: string;
- setRole: (role: 'teacher' | 'student') => void;
- };
- const ClassroomContext = createContext<ClassroomContextType | null>(null);
- // 辅助函数
- function 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('');
- }
- async function generateToken(
- 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);
- }
- function showToast(type: 'info' | 'success' | 'error', message: string): void {
- switch(type) {
- case 'info':
- toast.info(message);
- break;
- case 'success':
- toast.success(message);
- break;
- case 'error':
- toast.error(message);
- break;
- }
- }
- // 从SDK获取枚举值
- const { ImLogLevel, ImMessageLevel } = window.AliVCInteraction;
- // 配置信息
- 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 ClassroomPage = () => {
- // 状态管理
- const [userId, setUserId] = useState<string>('');
- const [role, setRole] = useState<'teacher' | 'student'>('student');
- 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>('');
- // 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([...messageList, text]);
- };
- const listenImEvents = (): void => {
- if (!imEngine.current) return;
- imEngine.current.on('connectsuccess', () => {
- showMessage('IM连接成功');
- });
- imEngine.current.on('disconnect', (code: number) => {
- showMessage(`IM断开连接: ${code}`);
- });
- };
- 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) => {
- showMessage(`收到消息: ${msg.data}`);
- });
- };
- // 音视频模块
- 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;
- aliRtcEngine.current.on('remoteUserOnLineNotify', (userId: string) => {
- showMessage(`用户 ${userId} 加入课堂`);
- });
- aliRtcEngine.current.on('remoteUserOffLineNotify', (userId: string) => {
- showMessage(`用户 ${userId} 离开课堂`);
- removeRemoteVideo(userId, 'camera');
- removeRemoteVideo(userId, 'screen');
- });
- };
- // 统一登录逻辑
- const login = async (userId: string): Promise<void> => {
- try {
- // 初始化IM
- const { ImEngine: ImEngineClass } = window.AliVCInteraction;
- imEngine.current = ImEngineClass.createEngine();
- await imEngine.current.init({
- deviceId: 'xxxx',
- appId: IM_APP_ID,
- appSign: IM_APP_SIGN,
- logLevel: ImLogLevel.ERROR,
- });
-
- // 初始化RTC
- 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;
- try {
- // 加入IM群组
- const gm = imEngine.current.getGroupManager();
- const mm = imEngine.current.getMessageManager();
- imGroupManager.current = gm || null;
- imMessageManager.current = mm || null;
- await gm!.joinGroup(classId);
- listenGroupEvents();
- listenMessageEvents();
- // 加入RTC频道
- const timestamp = Math.floor(Date.now() / 1000) + 3600 * 3;
- const token = await generateToken(RTC_APP_ID, RTC_APP_KEY, classId, userId, timestamp);
- aliRtcEngine.current.setChannelProfile(AliRtcSdkChannelProfile.AliRtcSdkCommunication);
- await aliRtcEngine.current.joinChannel(
- {
- channelId: classId,
- userId,
- appId: RTC_APP_ID,
- token,
- timestamp,
- },
- userId
- );
- // 设置本地预览
- aliRtcEngine.current.setLocalViewConfig('localPreviewer', AliRtcVideoTrack.AliRtcVideoTrackCamera);
-
- setIsJoinedClass(true);
- setErrorMessage('');
- showToast('success', '加入课堂成功');
- } catch (err: any) {
- setErrorMessage(`加入课堂失败: ${err.message}`);
- showToast('error', '加入课堂失败');
- }
- };
- // 离开课堂
- const leaveClass = async (): Promise<void> => {
- if (imGroupManager.current && classId) {
- await imGroupManager.current.leaveGroup(classId);
- }
- if (aliRtcEngine.current) {
- await aliRtcEngine.current.leaveChannel();
- }
-
- setIsJoinedClass(false);
- showToast('info', '已离开课堂');
- };
- // 发送消息
- const sendMessage = async (): Promise<void> => {
- if (!imMessageManager.current || !classId) return;
- try {
- await imMessageManager.current.sendGroupMessage({
- groupId: classId,
- data: msgText,
- type: 88888,
- level: ImMessageLevel.NORMAL,
- });
- setMsgText('');
- setErrorMessage('');
- } 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 (
- <ClassroomContext.Provider value={{
- userId,
- role,
- isLoggedIn,
- isJoinedClass,
- messageList,
- errorMessage,
- setRole,
- }}>
- <div className="container mx-auto p-4">
- <h1 className="text-2xl font-bold mb-4">互动课堂</h1>
-
- <ToastContainer
- position="top-right"
- autoClose={5000}
- hideProgressBar={false}
- newestOnTop={false}
- closeOnClick
- rtl={false}
- pauseOnFocusLoss
- draggable
- pauseOnHover
- />
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
- <div className="md:col-span-1">
- <form>
- <div className="mb-2">
- <label className="block text-sm font-medium text-gray-700">用户ID</label>
- <input
- className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm"
- value={userId}
- onChange={(e) => setUserId(e.target.value)}
- />
- </div>
-
- <div className="mb-2">
- <label className="block text-sm font-medium text-gray-700">课堂ID</label>
- <input
- className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm"
- value={classId}
- onChange={(e) => setClassId(e.target.value)}
- />
- </div>
-
- <div className="mb-2">
- <label className="block text-sm font-medium text-gray-700">角色</label>
- <select
- className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm"
- value={role}
- onChange={(e) => setRole(e.target.value as 'teacher' | 'student')}
- >
- <option value="student">学生</option>
- <option value="teacher">老师</option>
- </select>
- </div>
-
- <div className="flex space-x-2 mb-2">
- <button
- type="button"
- className="px-3 py-2 bg-blue-600 text-white rounded-md"
- disabled={isLoggedIn}
- onClick={() => login(userId)}
- >
- 登录
- </button>
-
- <button
- type="button"
- className="px-3 py-2 bg-blue-600 text-white rounded-md"
- disabled={!isLoggedIn || isJoinedClass}
- onClick={() => joinClass(classId)}
- >
- 加入课堂
- </button>
-
- <button
- type="button"
- className="px-3 py-2 bg-gray-600 text-white rounded-md"
- disabled={!isJoinedClass}
- onClick={leaveClass}
- >
- 离开课堂
- </button>
- </div>
- </form>
-
- <div className="mt-4">
- <label className="block text-sm font-medium text-gray-700">消息</label>
- <input
- className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm"
- value={msgText}
- onChange={(e) => setMsgText(e.target.value)}
- />
- <button
- type="button"
- className="mt-2 px-3 py-2 bg-blue-600 text-white rounded-md"
- disabled={!isJoinedClass}
- onClick={sendMessage}
- >
- 发送
- </button>
- </div>
- </div>
-
- <div className="md:col-span-1">
- <h4 className="text-lg font-medium mb-2">消息记录</h4>
- <div className="bg-gray-100 p-2 rounded-md h-64 overflow-y-auto">
- {messageList.map((msg, i) => (
- <div key={i} className="mb-1">{msg}</div>
- ))}
- </div>
- </div>
-
- <div className="md:col-span-1">
- <h4 className="text-lg font-medium mb-2">视频区域</h4>
- <video
- id="localPreviewer"
- muted
- className="w-full h-48 bg-black mb-2"
- ></video>
- <div
- id="remoteVideoContainer"
- ref={remoteVideoContainer}
- className="grid grid-cols-2 gap-2"
- ></div>
- </div>
- </div>
-
- {errorMessage && (
- <div className="mt-2 text-red-500">{errorMessage}</div>
- )}
- </div>
- </ClassroomContext.Provider>
- );
- };
|