Ver Fonte

将RTC频道加入逻辑移至startClass方法
确保老师点击"开始上课"后才加入RTC频道
保留了视频预览和消息通知功能
移除了createClass方法中的RTC相关代码

yourname há 7 meses atrás
pai
commit
11973c1939
2 ficheiros alterados com 263 adições e 12 exclusões
  1. 150 12
      client/mobile/pages_classroom.tsx
  2. 113 0
      rtc_analysis.md

+ 150 - 12
client/mobile/pages_classroom.tsx

@@ -149,6 +149,7 @@ export const ClassroomPage = () => {
 
   // 状态管理
   const [userId, setUserId] = useState<string>('');
+  const [isCameraOn, setIsCameraOn] = useState<boolean>(true);
   const [className, setClassName] = useState<string>('');
   const [role, setRole] = useState<Role>(Role.Student);
   const [classId, setClassId] = useState<string>('');
@@ -295,15 +296,84 @@ export const ClassroomPage = () => {
   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}`);
+      }
+    });
   };
 
   // 获取学生列表
@@ -366,7 +436,6 @@ export const ClassroomPage = () => {
       // 初始化RTC
       aliRtcEngine.current = AliRtcEngine.getInstance();
       AliRtcEngine.setLogLevel(0);
-      
       // 设置事件监听
       listenImEvents();
       listenRtcEvents();
@@ -401,6 +470,7 @@ export const ClassroomPage = () => {
       await gm!.joinGroup(classId);
       listenGroupEvents();
       listenMessageEvents();
+      listenRtcEvents();
 
       // 加入RTC频道
       const timestamp = Math.floor(Date.now() / 1000) + 3600 * 3;
@@ -420,6 +490,8 @@ export const ClassroomPage = () => {
       // 设置本地预览
       aliRtcEngine.current.setLocalViewConfig('localPreviewer', AliRtcVideoTrack.AliRtcVideoTrackCamera);
       
+      // 保留基础RTC连接,视频发布逻辑已移至startClass
+      
       setIsJoinedClass(true);
       setErrorMessage('');
       showToast('success', '加入课堂成功');
@@ -465,6 +537,38 @@ export const ClassroomPage = () => {
     if (!imMessageManager.current || !classId || role !== Role.Teacher) return;
     
     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({
         groupId: classId,
         data: JSON.stringify({ action: 'start_class' }),
@@ -551,6 +655,8 @@ export const ClassroomPage = () => {
       // 创建成功后自动加入群组
       try {
         await groupManager.joinGroup(response.groupId);
+        
+        
         showToast('success', '课堂创建并加入成功');
         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(() => {
     return () => {
@@ -960,17 +1084,31 @@ export const ClassroomPage = () => {
           </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 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>
         

+ 113 - 0
rtc_analysis.md

@@ -0,0 +1,113 @@
+# Aliyun RTC 推拉流实现分析
+
+## 1. 主流程概述
+```mermaid
+graph TD
+    A[初始化AliRtcEngine] --> B[生成Token]
+    B --> C[加入频道]
+    C --> D[设置本地预览]
+    D --> E[监听远端用户事件]
+    E --> F[处理订阅状态变化]
+    F --> G[管理视频元素生命周期]
+```
+
+## 2. 核心组件说明
+- **AliRtcEngine**: 阿里云RTC核心引擎,负责音视频通信
+  - 主要方法:
+    - `joinChannel()`: 加入音视频频道
+    - `setLocalViewConfig()`: 设置本地视频预览
+    - `setRemoteViewConfig()`: 设置远端视频渲染
+    - `leaveChannel()`: 离开频道
+  - 重要枚举:
+    - `AliRtcSubscribeState`: 订阅状态(1:未订阅, 3:已订阅)
+    - `AliRtcVideoTrack`: 视频轨道类型(Camera/Screen)
+
+## 3. 推流流程
+1. **本地预览设置**:
+```typescript
+// 加入频道成功后设置本地预览
+aliRtcEngine.current!.setLocalViewConfig(
+  'localPreviewer', 
+  AliRtcVideoTrack.AliRtcVideoTrackCamera
+);
+```
+2. **视频元素管理**:
+- 使用`<video id="localPreviewer">`元素显示本地摄像头画面
+- 通过`muted`属性静音本地音频避免回声
+
+## 4. 拉流订阅流程
+### 状态机转换逻辑
+```mermaid
+stateDiagram-v2
+    [*] --> 未订阅: 初始状态
+    未订阅 --> 已订阅: videoSubscribeStateChanged(3)
+    已订阅 --> 未订阅: videoSubscribeStateChanged(1)
+    
+    已订阅 --> 创建视频元素: newState=3
+    未订阅 --> 移除视频元素: newState=1
+```
+
+### 视频元素生命周期管理
+1. **创建时机**:
+```typescript
+// 当订阅状态变为3(已订阅)时创建视频元素
+if (newState === 3) {
+  const video = document.createElement('video');
+  video.autoplay = true;
+  remoteVideoElMap.current[vid] = video;
+  remoteVideoContainer.current?.appendChild(video);
+  aliRtcEngine.current!.setRemoteViewConfig(video, userId, trackType);
+}
+```
+2. **销毁时机**:
+```typescript
+function removeRemoteVideo(userId: string, type: 'camera' | 'screen') {
+  aliRtcEngine.current!.setRemoteViewConfig(null, userId, trackType);
+  el.pause();
+  remoteVideoContainer.current?.removeChild(el);
+  delete remoteVideoElMap.current[vid];
+}
+```
+
+## 5. 关键事件处理
+### remoteUserOnLineNotify
+- 触发条件: 远端用户加入频道
+- 处理逻辑: 显示通知,记录日志
+
+### remoteUserOffLineNotify 
+- 触发条件: 远端用户离开频道
+- 处理逻辑: 
+  - 显示通知
+  - 移除对应的视频元素
+  ```typescript
+  removeRemoteVideo(userId, 'camera');
+  removeRemoteVideo(userId, 'screen');
+  ```
+
+### videoSubscribeStateChanged
+- 参数说明:
+  - `userId`: 远端用户ID
+  - `oldState`: 旧状态
+  - `newState`: 新状态(3:订阅成功, 1:取消订阅)
+- 核心逻辑:
+  ```typescript
+  if (newState === 3) {
+    // 创建并配置视频元素
+  } else if (newState === 1) {
+    // 移除视频元素
+  }
+  ```
+
+## 6. 错误处理机制
+1. **加入频道失败**:
+```typescript
+try {
+  await aliRtcEngine.current.joinChannel(...);
+} catch (error) {
+  console.log('加入频道失败', error);
+  showToast('error', '加入频道失败');
+}
+```
+2. **全局错误监听**:
+- 通过`on('bye')`事件处理异常退出
+- 通过Toast组件显示错误信息