|
@@ -149,6 +149,7 @@ export const ClassroomPage = () => {
|
|
|
|
|
|
|
|
// 状态管理
|
|
// 状态管理
|
|
|
const [userId, setUserId] = useState<string>('');
|
|
const [userId, setUserId] = useState<string>('');
|
|
|
|
|
+ const [isCameraOn, setIsCameraOn] = useState<boolean>(true);
|
|
|
const [className, setClassName] = useState<string>('');
|
|
const [className, setClassName] = useState<string>('');
|
|
|
const [role, setRole] = useState<Role>(Role.Student);
|
|
const [role, setRole] = useState<Role>(Role.Student);
|
|
|
const [classId, setClassId] = useState<string>('');
|
|
const [classId, setClassId] = useState<string>('');
|
|
@@ -295,15 +296,84 @@ export const ClassroomPage = () => {
|
|
|
const listenRtcEvents = () => {
|
|
const listenRtcEvents = () => {
|
|
|
if (!aliRtcEngine.current) return;
|
|
if (!aliRtcEngine.current) return;
|
|
|
|
|
|
|
|
|
|
+ showMessage('注册rtc事件监听')
|
|
|
|
|
+
|
|
|
aliRtcEngine.current.on('remoteUserOnLineNotify', (userId: string) => {
|
|
aliRtcEngine.current.on('remoteUserOnLineNotify', (userId: string) => {
|
|
|
showMessage(`用户 ${userId} 加入课堂`);
|
|
showMessage(`用户 ${userId} 加入课堂`);
|
|
|
|
|
+ console.log('用户上线通知:', userId);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
aliRtcEngine.current.on('remoteUserOffLineNotify', (userId: string) => {
|
|
aliRtcEngine.current.on('remoteUserOffLineNotify', (userId: string) => {
|
|
|
showMessage(`用户 ${userId} 离开课堂`);
|
|
showMessage(`用户 ${userId} 离开课堂`);
|
|
|
|
|
+ console.log('用户下线通知:', userId);
|
|
|
removeRemoteVideo(userId, 'camera');
|
|
removeRemoteVideo(userId, 'camera');
|
|
|
removeRemoteVideo(userId, 'screen');
|
|
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}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 获取学生列表
|
|
// 获取学生列表
|
|
@@ -366,7 +436,6 @@ export const ClassroomPage = () => {
|
|
|
// 初始化RTC
|
|
// 初始化RTC
|
|
|
aliRtcEngine.current = AliRtcEngine.getInstance();
|
|
aliRtcEngine.current = AliRtcEngine.getInstance();
|
|
|
AliRtcEngine.setLogLevel(0);
|
|
AliRtcEngine.setLogLevel(0);
|
|
|
-
|
|
|
|
|
// 设置事件监听
|
|
// 设置事件监听
|
|
|
listenImEvents();
|
|
listenImEvents();
|
|
|
listenRtcEvents();
|
|
listenRtcEvents();
|
|
@@ -401,6 +470,7 @@ export const ClassroomPage = () => {
|
|
|
await gm!.joinGroup(classId);
|
|
await gm!.joinGroup(classId);
|
|
|
listenGroupEvents();
|
|
listenGroupEvents();
|
|
|
listenMessageEvents();
|
|
listenMessageEvents();
|
|
|
|
|
+ listenRtcEvents();
|
|
|
|
|
|
|
|
// 加入RTC频道
|
|
// 加入RTC频道
|
|
|
const timestamp = Math.floor(Date.now() / 1000) + 3600 * 3;
|
|
const timestamp = Math.floor(Date.now() / 1000) + 3600 * 3;
|
|
@@ -420,6 +490,8 @@ export const ClassroomPage = () => {
|
|
|
// 设置本地预览
|
|
// 设置本地预览
|
|
|
aliRtcEngine.current.setLocalViewConfig('localPreviewer', AliRtcVideoTrack.AliRtcVideoTrackCamera);
|
|
aliRtcEngine.current.setLocalViewConfig('localPreviewer', AliRtcVideoTrack.AliRtcVideoTrackCamera);
|
|
|
|
|
|
|
|
|
|
+ // 保留基础RTC连接,视频发布逻辑已移至startClass
|
|
|
|
|
+
|
|
|
setIsJoinedClass(true);
|
|
setIsJoinedClass(true);
|
|
|
setErrorMessage('');
|
|
setErrorMessage('');
|
|
|
showToast('success', '加入课堂成功');
|
|
showToast('success', '加入课堂成功');
|
|
@@ -465,6 +537,38 @@ export const ClassroomPage = () => {
|
|
|
if (!imMessageManager.current || !classId || role !== Role.Teacher) return;
|
|
if (!imMessageManager.current || !classId || role !== Role.Teacher) return;
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
|
|
+ // 确保RTC连接已建立
|
|
|
|
|
+ if (!aliRtcEngine.current) {
|
|
|
|
|
+ throw new Error('RTC连接未建立');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 加入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
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 开启老师视频
|
|
|
|
|
+ try {
|
|
|
|
|
+ aliRtcEngine.current!.setLocalViewConfig('localPreviewer', AliRtcVideoTrack.AliRtcVideoTrackCamera);
|
|
|
|
|
+ await aliRtcEngine.current.startPreview();
|
|
|
|
|
+ console.log('老师视频已开启');
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('开启老师视频失败:', err);
|
|
|
|
|
+ showToast('error', '开启视频失败');
|
|
|
|
|
+ throw err;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 发送开始上课消息
|
|
|
await imMessageManager.current.sendGroupMessage({
|
|
await imMessageManager.current.sendGroupMessage({
|
|
|
groupId: classId,
|
|
groupId: classId,
|
|
|
data: JSON.stringify({ action: 'start_class' }),
|
|
data: JSON.stringify({ action: 'start_class' }),
|
|
@@ -551,6 +655,8 @@ export const ClassroomPage = () => {
|
|
|
// 创建成功后自动加入群组
|
|
// 创建成功后自动加入群组
|
|
|
try {
|
|
try {
|
|
|
await groupManager.joinGroup(response.groupId);
|
|
await groupManager.joinGroup(response.groupId);
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
showToast('success', '课堂创建并加入成功');
|
|
showToast('success', '课堂创建并加入成功');
|
|
|
showMessage(`课堂 ${className} 创建成功,ID: ${response.groupId}`);
|
|
showMessage(`课堂 ${className} 创建成功,ID: ${response.groupId}`);
|
|
|
|
|
|
|
@@ -587,6 +693,24 @@ export const ClassroomPage = () => {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // 切换摄像头状态
|
|
|
|
|
+ const toggleCamera = async () => {
|
|
|
|
|
+ if (!aliRtcEngine.current) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (isCameraOn) {
|
|
|
|
|
+ await aliRtcEngine.current.stopPreview();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ await aliRtcEngine.current.startPreview();
|
|
|
|
|
+ }
|
|
|
|
|
+ setIsCameraOn(!isCameraOn);
|
|
|
|
|
+ showToast('info', `摄像头已${isCameraOn ? '关闭' : '开启'}`);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('切换摄像头状态失败:', err);
|
|
|
|
|
+ showToast('error', '切换摄像头失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
// 清理资源
|
|
// 清理资源
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
return () => {
|
|
return () => {
|
|
@@ -960,17 +1084,31 @@ export const ClassroomPage = () => {
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div className="md:col-span-1">
|
|
<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 className="mb-4">
|
|
|
|
|
+ <h4 className="text-lg font-medium mb-2">本地视频</h4>
|
|
|
|
|
+ <div className="relative">
|
|
|
|
|
+ <video
|
|
|
|
|
+ id="localPreviewer"
|
|
|
|
|
+ muted
|
|
|
|
|
+ className="w-full h-48 bg-black"
|
|
|
|
|
+ ></video>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={toggleCamera}
|
|
|
|
|
+ className="absolute bottom-2 right-2 px-3 py-1 bg-blue-600 text-white rounded-md"
|
|
|
|
|
+ >
|
|
|
|
|
+ {isCameraOn ? '关闭摄像头' : '开启摄像头'}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h4 className="text-lg font-medium mb-2">远程视频</h4>
|
|
|
|
|
+ <div
|
|
|
|
|
+ id="remoteVideoContainer"
|
|
|
|
|
+ ref={remoteVideoContainer}
|
|
|
|
|
+ className="grid grid-cols-2 gap-2"
|
|
|
|
|
+ ></div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|