Selaa lähdekoodia

update 直播 群组讨论

dusenyao 3 vuotta sitten
vanhempi
commit
c12302c7a1

+ 75 - 0
src/api/live.js

@@ -150,3 +150,78 @@ export function GetStudentList_FinishMaterial(data) {
     data
   });
 }
+
+/**
+ * 开始分组讨论
+ * @param {Object} data {task_id 任务ID}
+ */
+export function StartGroup(data) {
+  let params = getRequestParams('live_room-live_room_dispatch-StartGroup');
+
+  return request({
+    method: 'post',
+    url: process.env.VUE_APP_LearnWebSI,
+    params,
+    data
+  });
+}
+
+/**
+ * 是否开启分组讨论
+ * @param {Object} data {task_id 任务ID}
+ */
+export function IsEnableGroup(data) {
+  let params = getRequestParams('live_room-live_room_dispatch-IsEnableGroup');
+
+  return request({
+    method: 'post',
+    url: process.env.VUE_APP_LearnWebSI,
+    params,
+    data
+  });
+}
+
+/**
+ * 得到分组讨论信息(教师端)
+ * @param {Object} data {task_id 任务ID}
+ */
+export function GetGroupInfo_Teacher(data) {
+  let params = getRequestParams('live_room-live_room_dispatch-GetGroupInfo_Teacher');
+
+  return request({
+    method: 'post',
+    url: process.env.VUE_APP_LearnWebSI,
+    params,
+    data
+  });
+}
+
+/**
+ * 得到我的分组讨论信息(学员端)
+ * @param {Object} data {task_id 任务ID}
+ */
+export function GetMyGroupInfo_Student(data) {
+  let params = getRequestParams('live_room-live_room_dispatch-GetMyGroupInfo_Student');
+
+  return request({
+    method: 'post',
+    url: process.env.VUE_APP_LearnWebSI,
+    params,
+    data
+  });
+}
+
+/**
+ * 结束分组讨论
+ * @param {Object} data {task_id 任务ID}
+ */
+export function StopGroup(data) {
+  let params = getRequestParams('live_room-live_room_dispatch-StopGroup');
+
+  return request({
+    method: 'post',
+    url: process.env.VUE_APP_LearnWebSI,
+    params,
+    data
+  });
+}

+ 9 - 0
src/icons/svg/group.svg

@@ -0,0 +1,9 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M24 0H0V24H24V0Z" fill="white" fill-opacity="0.01"/>
+<path d="M7 17C8.38071 17 9.5 15.8807 9.5 14.5C9.5 13.1193 8.38071 12 7 12C5.61929 12 4.5 13.1193 4.5 14.5C4.5 15.8807 5.61929 17 7 17Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M17 17C18.3807 17 19.5 15.8807 19.5 14.5C19.5 13.1193 18.3807 12 17 12C15.6193 12 14.5 13.1193 14.5 14.5C14.5 15.8807 15.6193 17 17 17Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 7C13.3807 7 14.5 5.88071 14.5 4.5C14.5 3.11929 13.3807 2 12 2C10.6193 2 9.5 3.11929 9.5 4.5C9.5 5.88071 10.6193 7 12 7Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 22C12 19.2386 9.7614 17 7 17C4.23857 17 2 19.2386 2 22" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M22 22C22 19.2386 19.7614 17 17 17C14.2386 17 12 19.2386 12 22" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M17 12C17 9.2386 14.7614 7 12 7C9.2386 7 7 9.2386 7 12" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
src/router/index.js

@@ -119,6 +119,10 @@ const routes = [
       {
         path: '/live/student',
         component: () => import('@/views/live/student')
+      },
+      {
+        path: '/live/group',
+        component: () => import('@/views/live/group')
       }
     ]
   },

+ 2 - 2
src/views/live/common.js

@@ -34,7 +34,7 @@ export function handsDown(Object) {
  * 上麦更新
  * @param { Object } stream
  */
-function updateMcResult(stid, pid) {
+export function updateMcResult(stid, pid) {
   // 上麦更新是在两种情况下执行
   // 1.推流成功之后将自己的麦序更新为3;系统会广播 speak_context 事件,其他用户监听该事件后更新对应的业务展示状态;
   // 2.不能创建本地流或者推流失败,更新自己的麦序为0。
@@ -243,7 +243,7 @@ export function downloadWebSDK(vue) {
     vue.loadedNumber += 1;
   };
 
-  createScript('https://class.csslcloud.net/static/dist/js/websdk_4.0.js').onload = () => {
+  createScript('https://class.csslcloud.net/sdk/websdk/hdRtc-6.7.2.js').onload = () => {
     let scriptArray = [
       'https://class.csslcloud.net/static/dist/js/classMode.js',
       'https://class.csslcloud.net/static/dist/js/classUpdateChat.js',

+ 242 - 0
src/views/live/group/group.js

@@ -0,0 +1,242 @@
+import { Message } from 'element-ui';
+import { rtc, publishStream, closeVideo } from '@/views/live/common';
+export {
+  initSDK,
+  sendMsg,
+  getLiveStat,
+  getDevice,
+  createScript,
+  downloadWebSDK,
+  closeVideo
+} from '@/views/live/common';
+
+/**
+ * 取消订阅远程流
+ */
+function unSubscribeStream(stream) {
+  rtc.unSubscribeStream({
+    unSubStream: stream,
+    success: function (str) {
+      console.log('取消订阅流成功', str);
+    },
+
+    fail: function (str) {
+      console.log(str);
+    }
+  });
+}
+
+/**
+ * 初始化监听事件
+ */
+export function initListener(vue) {
+  rtc.on('login_success', data => {
+    console.log('登录成功', data);
+    Message({
+      message: '登录成功',
+      type: 'success'
+    });
+  });
+
+  rtc.on('login_failed', data => {
+    console.log('登录失败', data);
+    Message({
+      message: '登录失败:' + JSON.stringify(data),
+      type: 'warning'
+    });
+  });
+
+  // 教师 必须在加入房间成功的事件回调里创建本地流
+  rtc.on('conference_join', () => {
+    console.log('加入房间成功');
+    rtc.handsUp({
+      success: str => {
+        console.log('举手成功', str);
+      },
+      fail: data => {
+        console.log('举手失败', data);
+      }
+    });
+  });
+
+  rtc.on('conference_join_failed', err => {
+    // 加入房间失败  err为错误原因
+    console.log('加入房间失败', err);
+  });
+
+  // 新增订阅流事件
+  rtc.on('allow_sub', stream => {
+    console.log('新增订阅流事件');
+    if (stream.isMixed()) {
+      console.log('是混合流,不订阅');
+    } else {
+      // 订阅远程流
+      rtc.trySubscribeStream({
+        tryStream: stream,
+        success: stream => {
+          // 订阅流成功
+          let streamType = stream.streamType();
+          console.log('订阅流成功', streamType);
+          if (streamType === 10) {
+            vue.streamList.push(stream);
+          }
+          if (streamType === 0) {
+            stream.show('live', 'contain'); // 将流显示到指定 id 的元素中
+          }
+        },
+        fail: function (err) {
+          console.log('订阅流失败', err);
+        }
+      });
+    }
+  });
+
+  // 推流前查询直播状态失败,导致没有推流
+  rtc.on('local_stream_publish_failed', function () {
+    console.log('推流前查询直播状态失败,导致没有推流');
+    Message({
+      message: '推流前查询直播状态失败,导致没有推流',
+      type: 'warning'
+    });
+  });
+
+  // 房间全量信息事件(人员进出时广播)
+  rtc.on('room_context', roomData => {
+    vue.roomContext = JSON.parse(roomData);
+    vue.getLiveRoomStudentList();
+    console.log('房间全量信息事件(人员进出时广播)', JSON.parse(roomData));
+  });
+
+  rtc.on('publish_stream', str => {
+    console.log('直播已开启', str);
+    Message({
+      type: 'success',
+      message: '直播已开启'
+    });
+    vue.liveStat = true;
+  });
+
+  rtc.on('end_stream', str => {
+    console.log('直播已关闭', str);
+    Message({
+      type: 'success',
+      message: '直播已关闭'
+    });
+    vue.liveStat = false;
+  });
+
+  // 单个用户配置监听
+  rtc.on('switch_user_settings', settingData => {
+    console.log('单个用户配置监听', JSON.parse(settingData));
+  });
+
+  // 人员列表事件(人员麦序变化时广播)
+  rtc.on('speak_context', speakData => {
+    vue.speakData = JSON.parse(speakData);
+    console.log('人员列表事件(人员麦序变化时广播)', JSON.parse(speakData));
+  });
+
+  rtc.on('switch_settings', data => {
+    console.log('房间设置事件', JSON.parse(data)); // 房间设置事件
+  });
+
+  rtc.on('publishStreamErr', data => {
+    console.log('推流意外终止:' + data.streamName);
+    // 直播开启状态下,尝试重推这条流
+  });
+
+  rtc.on('kick_out', function () {
+    console.log('自己被踢出房间');
+  });
+
+  // 视频无法自动播放
+  rtc.on('playError', data => {
+    console.log('视频无法自动播放', data);
+  });
+
+  // 监听通知移除流事件
+  rtc.on('stream_removed', stream => {
+    console.log('监听通知移除流事件');
+    console.log(stream.id());
+    let num = vue.streamList.findIndex(el => el.id() === stream.id());
+    if (num !== -1) {
+      vue.streamList.splice(num, 1);
+    }
+  });
+
+  // 停止订阅流
+  rtc.on('unSub', stream => {
+    console.log('停止订阅流');
+    unSubscribeStream(stream);
+  });
+
+  /**
+   * 排麦监听事件
+   */
+
+  // 监听自己被邀请事件
+  rtc.on('inviteUp', uid => {
+    console.log('监听自己被邀请事件', uid);
+  });
+
+  rtc.on('mcDown', () => {
+    closeVideo('picture');
+  });
+
+  rtc.on('videoModeChange', data => {
+    console.log('连麦音视频模式更新成功监听回调', data.settings.video_mode);
+    vue.roomInfo.video_mode = data.settings.video_mode;
+  });
+
+  rtc.on('createLocalStream', () => {
+    // 创建本地流推流
+    console.log('创建本地流推流');
+    const createData = {
+      video: true,
+      audio: true
+    };
+    rtc.createLocalStream({
+      streamName: 'picture',
+      createData,
+      success: function (stream) {
+        console.log('创建本地流成功', stream);
+        // 创建本地流成功,将流展示到id为 student 的dom元素盒子中
+        stream.show('group-local');
+        publishStream('picture'); // 如果需要立即推流,执行 publish 方法
+      },
+      fail: function (data) {
+        console.log('创建本地流失败,应用层处理', data);
+        // 创建本地流失败,应用层处理
+        Message({
+          type: 'error',
+          message: '创建本地流失败:' + data
+        });
+      }
+    });
+  });
+
+  /**
+   * 监听聊天事件
+   */
+  rtc.on('chat_message', data => {
+    let dat = JSON.parse(data);
+    console.log(dat);
+    // 敏感词过滤:如果发送的聊天消息被系统判定包含敏感词,则只有发送者能收到本条消息,房间内其他人都不会收到这条聊天消息。
+    // 如果返回消息中有 isFilterChat 字段(消息不包含敏感词返回数据中无isFilterChat字段),且isFilterChat的值为1,则说明该消息包含敏感字,除发送者外其他人不会收到这条消息。
+    if (dat.isFilterChat && dat.isFilterChat === 1) {
+      return;
+    }
+    vue.chatList.push(dat);
+  });
+
+  rtc.on('allowChatChange', function (data) {
+    let msg = data.settings.allow_chat ? '开言' : '禁言';
+    Message({
+      type: 'success',
+      message: `全体${msg}成功`
+    });
+  });
+
+  // 接收自定义消息
+  rtc.on('publish_message', data => {});
+}

+ 517 - 0
src/views/live/group/index.vue

@@ -0,0 +1,517 @@
+<template>
+  <div class="group">
+    <!--顶部-->
+    <div class="group-top">
+      <div class="live-title">
+        <div class="live-title-name">{{ roomInfo.cs_item_name }} {{ roomInfo.task_name }}</div>
+        <div>
+          <el-button @click="exitRoom">退出房间</el-button>
+          <el-button @click="getLiveStat">getLiveStat</el-button>
+        </div>
+      </div>
+      <div class="live-course-name">{{ roomInfo.course_name }}</div>
+      <div class="live-teacher">
+        <span class="live-teacher-name">
+          <svg-icon icon-class="person" />{{ roomInfo.teacher_name }}
+        </span>
+        <span><svg-icon icon-class="people" />{{ roomInfo.student_count }}</span>
+      </div>
+    </div>
+    <!-- 主容器 -->
+    <div class="group-container">
+      <!-- 左侧 -->
+      <div class="group-container-left">
+        <div class="group-discussion">
+          <div id="group-local" class="group-box"></div>
+          <div
+            v-for="(item, i) in streamList"
+            :id="`group-${i}`"
+            :key="item.id()"
+            class="group-box"
+          ></div>
+        </div>
+        <div class="button-group">
+          <div class="button-group-left"></div>
+          <div class="button-group-right"></div>
+        </div>
+        <div class="group-container-left-chat">
+          <div class="chat-top">
+            <span>聊天</span>
+          </div>
+          <div class="chat-window">
+            <ul class="chat-window-ul">
+              <li v-for="(item, i) in chatList" :key="i">
+                <div class="msg-normal">
+                  <span>{{ item.username }}: </span>
+                  <span>{{ item.msg }}</span>
+                </div>
+              </li>
+            </ul>
+          </div>
+          <div class="chat-speak">
+            <el-input
+              v-model="msg"
+              placeholder="输入发言"
+              maxlength="400"
+              @keydown.enter.native="sendMsg"
+            >
+              <el-button slot="append" @click="sendMsg">发送</el-button>
+            </el-input>
+          </div>
+        </div>
+      </div>
+      <!-- 右侧 -->
+      <div class="group-container-right">
+        <div
+          class="live-teacher-lens"
+          @mouseover="liveMenuShow = true"
+          @mouseout="liveMenuShow = false"
+        >
+          <div id="live"></div>
+          <div :style="{ bottom: liveMenuShow ? '0' : '-40px' }" class="live-wrapper">
+            <div>
+              {{ roomInfo.teacher_name }}
+            </div>
+            <div></div>
+          </div>
+        </div>
+        <div class="student-list">
+          <div class="student-list-title">学生列表</div>
+          <ul>
+            <li v-for="item in student_list" :key="item.room_user_id">
+              <div class="student-list-left">
+                <el-avatar icon="el-icon-user" size="small" :src="item.student_image_url" />
+                <span class="name">{{ item.student_name }}</span>
+              </div>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import {
+  GetLiveRoomStudentList,
+  StudentExitLiveRoom,
+  GetLiveRoomInfo,
+  IsEnableGroup,
+  GetMyGroupInfo_Student
+} from '@/api/live';
+import * as common from './group';
+
+export default {
+  data() {
+    return {
+      task_id: this.$route.query.task_id,
+      room_user_id: this.$route.query.live_room_sys_user_id,
+      // 定时器
+      timer: null,
+      timer_group: null,
+      rtc: null,
+      streamList: [],
+      roomData: {
+        desc: '直播间标题',
+        name: '姓名',
+        user: {
+          id: '',
+          name: '',
+          role: 'talker',
+          rommid: ''
+        },
+        max_users: 1,
+        allow_chat: true,
+        allow_audio: true,
+        allow_speak: true
+      },
+      roomInfo: {
+        room_id: '',
+        video_mode: 1,
+        task_name: '',
+        cs_item_name: '',
+        course_name: '',
+        teacher_name: '',
+        student_count: 0,
+        teacher_image_url: ''
+      },
+      loadedNumber: 0,
+      speakData: {},
+      roomContext: {},
+      msg: '',
+      chatList: [],
+      // 直播间学员列表
+      student_list: [],
+      // 直播状态
+      liveStat: false,
+      liveMenuShow: false
+    };
+  },
+  watch: {
+    loadedNumber(newVal) {
+      if (newVal === 5) {
+        common.createScript(
+          'https://class.csslcloud.net/static/SDK/docSDK/drawSdk_3.0.js'
+        ).onload = () => {
+          const { live_room_sys_user_id, room_id, sessionid } = this.$route.query;
+          this.rtc = common.initSDK({
+            userid: live_room_sys_user_id,
+            roomid: room_id,
+            sessionid
+          });
+          common.initListener(this); // 注册监听事件
+          this.getLiveStat();
+          this.$loading().close();
+        };
+      }
+    },
+    streamList(newVal) {
+      if (newVal.length > 0) {
+        this.$nextTick(() => {
+          newVal[newVal.length - 1].show(`group-${newVal.length - 1}`);
+        });
+      }
+    }
+  },
+  created() {
+    this.getLiveRoomStudentList();
+    this.getLiveRoomInfo();
+  },
+  mounted() {
+    // const { live_room_sys_user_id, room_id, sessionid } = this.$route.query;
+    // this.rtc = common.initSDK({
+    //   userid: live_room_sys_user_id,
+    //   roomid: room_id,
+    //   sessionid
+    // });
+    // common.initListener(this); // 注册监听事件
+    // this.getLiveStat();
+    common.downloadWebSDK(this);
+    this.getLiveRoomStudentListPolling();
+    this.isEnableGroup();
+  },
+  beforeDestroy() {
+    clearInterval(this.timer);
+    clearInterval(this.timer_group);
+    StudentExitLiveRoom({ task_id: this.task_id, room_user_id: this.room_user_id });
+    common.closeVideo('picture');
+  },
+  methods: {
+    getLiveRoomInfo() {
+      GetLiveRoomInfo({ task_id: this.task_id }).then(
+        ({
+          room_id,
+          video_mode,
+          task_name,
+          cs_item_name,
+          course_name,
+          teacher_name,
+          student_count,
+          teacher_image_url
+        }) => {
+          this.roomInfo = {
+            room_id,
+            video_mode,
+            task_name,
+            cs_item_name,
+            course_name,
+            teacher_name,
+            student_count,
+            teacher_image_url
+          };
+        }
+      );
+    },
+
+    exitRoom() {
+      StudentExitLiveRoom({ task_id: this.task_id, room_user_id: this.room_user_id }).then(() => {
+        this.$router.push('/');
+      });
+    },
+
+    getLiveStat() {
+      common.getLiveStat({
+        success: data => {
+          this.liveStat = data.started;
+        },
+        fail: str => {
+          this.liveStat = false;
+          console.log('直播关闭状态或查询直播失败', str);
+        }
+      });
+    },
+
+    // 发消息
+    sendMsg() {
+      common.sendMsg(this.msg);
+      this.msg = '';
+    },
+
+    getDevice() {
+      common.getDevice();
+    },
+
+    getLiveRoomStudentList() {
+      GetLiveRoomStudentList({ task_id: this.task_id }).then(({ student_list }) => {
+        this.student_list = student_list;
+      });
+    },
+
+    getLiveRoomStudentListPolling() {
+      this.timer = setInterval(() => {
+        this.getLiveRoomStudentList();
+      }, 5000);
+    },
+
+    // 分组讨论
+    isEnableGroup() {
+      this.timer_group = setInterval(() => {
+        IsEnableGroup({ task_id: this.task_id })
+          .then(({ is_enable_group }) => {
+            if (is_enable_group === 'true') {
+              clearInterval(this.timer_group);
+              this.getMyGroupInfo_Student();
+            }
+          })
+          .catch(() => {
+            clearInterval(this.timer_group);
+          });
+      }, 5000);
+    },
+
+    getMyGroupInfo_Student() {
+      GetMyGroupInfo_Student({
+        task_id: this.task_id
+      }).then(({ student_list }) => {
+        let data = student_list.find(el => {
+          return el.is_self === 'true';
+        });
+        console.log(data);
+      });
+    }
+  }
+};
+</script>
+
+<style lang="scss">
+@import '~@/styles/mixin.scss';
+$live-bc: #3d3938;
+
+.group {
+  @include container;
+
+  // 顶部
+  &-top {
+    background-color: #fff;
+    padding: 24px 32px;
+    border-top-left-radius: 8px;
+    border-top-right-radius: 8px;
+
+    .live-title {
+      display: flex;
+      justify-content: space-between;
+
+      &-name {
+        font-size: 22px;
+      }
+
+      .el-button {
+        border-radius: 4px;
+        padding: 7px 12px;
+      }
+    }
+
+    .live-course-name {
+      font-size: 14px;
+      color: #737373;
+      line-height: 30px;
+    }
+
+    .live-teacher {
+      margin-top: 12px;
+
+      .svg-icon {
+        margin-right: 8px;
+      }
+
+      &-name {
+        margin-right: 60px;
+      }
+    }
+  }
+
+  // 主容器
+  &-container {
+    display: flex;
+    justify-content: left;
+
+    &-left {
+      width: 832px;
+      background-color: #fff;
+      border-radius: 8px;
+
+      // 分组讨论
+      .group-discussion {
+        display: flex;
+        flex-wrap: wrap;
+        width: 100%;
+        height: 468px;
+        position: relative;
+        background-color: $live-bc;
+        overflow: hidden;
+
+        .group-box {
+          width: 256px;
+          height: 144px;
+          margin: 8px;
+        }
+      }
+
+      .button-group {
+        display: flex;
+        justify-content: space-between;
+        height: 48px;
+        background-color: #4d4d4d;
+        padding: 0 15px;
+        border-bottom-left-radius: 5px;
+
+        .svg-icon {
+          font-size: 20px;
+        }
+
+        &-left {
+          > span {
+            display: inline-block;
+            height: 100%;
+            padding: 14px 16px;
+            cursor: pointer;
+
+            &:active,
+            &:hover {
+              background-color: #3d3d3d;
+            }
+          }
+        }
+      }
+
+      // 聊天窗口
+      &-chat {
+        height: 278px;
+        border: 1px solid #ccc;
+        border-bottom-left-radius: 8px;
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+
+        .chat-top {
+          display: flex;
+          justify-content: space-between;
+          padding: 15px 15px 10px;
+          border-bottom: 1px solid #e6e6e6;
+          color: #959595;
+
+          label {
+            cursor: pointer;
+          }
+
+          .allow-chat {
+            margin-right: 12px;
+          }
+        }
+
+        .chat-window {
+          position: relative;
+          width: 100%;
+          height: 100%;
+          overflow: hidden;
+
+          &-ul {
+            position: absolute;
+            top: 0;
+            left: 0;
+            width: 100%;
+            height: 100%;
+            overflow: auto;
+
+            .msg-normal {
+              padding: 7px 16px;
+            }
+          }
+        }
+
+        .chat-speak {
+          padding: 16px;
+        }
+      }
+    }
+
+    &-right {
+      padding: 8px;
+      background-color: #2c2c2c;
+      border-end-end-radius: 8px;
+
+      .live-teacher-lens {
+        position: relative;
+        overflow: hidden;
+
+        #live {
+          width: 352px;
+          height: 198px;
+          background-color: $live-bc;
+        }
+
+        .live-wrapper {
+          position: absolute;
+          height: 40px;
+          width: 100%;
+          background-color: #000;
+          opacity: 0.7;
+          color: #fff;
+          line-height: 40px;
+          padding: 0 16px;
+          transition: all 300ms ease-in 0s;
+        }
+      }
+
+      .student-list {
+        width: 100%;
+        padding: 24px 16px;
+        margin-top: 2px;
+        height: calc(100% - 200px);
+        background-color: #2c2c2c;
+        font-size: 14px;
+        color: #fff;
+
+        &-title {
+          margin-bottom: 16px;
+        }
+
+        li {
+          display: flex;
+          margin-bottom: 16px;
+
+          .student-list-left {
+            flex: 7;
+
+            .name {
+              vertical-align: super;
+              margin-left: 8px;
+            }
+          }
+
+          .student-list-right {
+            flex: 3;
+
+            .svg-icon {
+              font-size: 18px;
+              cursor: pointer;
+              margin-top: 7px;
+              margin-right: 8px;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 62 - 37
src/views/live/student/index.vue

@@ -54,25 +54,7 @@
             </template>
           </template>
         </div>
-        <div v-show="isDraw" id="draw-parent">
-          <div v-show="isDrawSetting" class="draw-setting">
-            <span class="brush-shape">
-              <svg-icon icon-class="brush-shape" />
-            </span>
-            <span
-              v-for="item in drawColorList"
-              :key="item"
-              :class="['draw-color', item === '#FF4747' ? 'current' : '']"
-              :style="{ 'background-color': item }"
-            ></span>
-            <span v-for="item in drawThicknessList" :key="item" class="draw-thickness">
-              <span :style="{ width: item * 2 + 'px', height: item * 2 + 'px' }"></span>
-            </span>
-            <span class="brush-clear">
-              <svg-icon icon-class="clear" />
-            </span>
-          </div>
-        </div>
+        <div v-show="isDraw" id="draw-parent"></div>
         <div class="button-group">
           <div class="button-group-left"></div>
           <div class="button-group-right"></div>
@@ -150,7 +132,9 @@ import {
   GetLiveRoomStudentList,
   StudentExitLiveRoom,
   GetCurMaterialSent,
-  GetLiveRoomInfo
+  GetLiveRoomInfo,
+  IsEnableGroup,
+  GetMyGroupInfo_Student
 } from '@/api/live';
 import CurMaterial from '@/components/live/CurMaterial.vue';
 import * as common from './live';
@@ -178,6 +162,7 @@ export default {
       material_picture_url: '',
       // 定时器
       timer: null,
+      timer_group: null,
       remoteStreamType: -1,
       rtc: null,
       roomData: {
@@ -209,9 +194,6 @@ export default {
       roomContext: {},
       msg: '',
       chatList: [],
-      isDrawSetting: false,
-      drawColorList: ['#FF4747', '#343434', '#628EFF', '#FFCA0E'],
-      drawThicknessList: ['1', '3', '5'],
       // 直播间学员列表
       student_list: [],
       // 直播状态
@@ -231,7 +213,14 @@ export default {
         common.createScript(
           'https://class.csslcloud.net/static/SDK/docSDK/drawSdk_3.0.js'
         ).onload = () => {
-          this.initSDK();
+          const { live_room_sys_user_id, room_id, session_id } = this.$route.query;
+          this.rtc = common.initSDK({
+            userid: live_room_sys_user_id,
+            roomid: room_id,
+            sessionid: session_id
+          });
+          common.initListener(this); // 注册监听事件
+          this.getLiveStat();
           this.$loading().close();
         };
       }
@@ -249,6 +238,7 @@ export default {
   mounted() {
     this.getCurMaterialSent();
     this.getLiveRoomStudentListPolling();
+    this.isEnableGroup();
   },
   beforeDestroy() {
     // 清除所有定时器
@@ -257,23 +247,13 @@ export default {
     //   clearInterval(i);
     // }
     clearInterval(this.timer);
+    clearInterval(this.timer_group);
     StudentExitLiveRoom({ task_id: this.task_id, room_user_id: this.room_user_id });
     if (this.callLoading || this.connect) {
       this.handsDown();
     }
   },
   methods: {
-    initSDK() {
-      const { live_room_sys_user_id, room_id, session_id } = this.$route.query;
-      this.rtc = common.initSDK({
-        userid: live_room_sys_user_id,
-        roomid: room_id,
-        sessionid: session_id
-      });
-      common.initListener(this); // 注册监听事件
-      this.getLiveStat();
-    },
-
     getLiveRoomInfo() {
       GetLiveRoomInfo({ task_id: this.task_id }).then(
         ({
@@ -370,6 +350,41 @@ export default {
       }, 5000);
     },
 
+    // 分组讨论
+    isEnableGroup() {
+      this.timer_group = setInterval(() => {
+        IsEnableGroup({ task_id: this.task_id })
+          .then(({ is_enable_group }) => {
+            if (is_enable_group === 'true') {
+              clearInterval(this.timer_group);
+              this.getMyGroupInfo_Student();
+            }
+          })
+          .catch(() => {
+            clearInterval(this.timer_group);
+          });
+      }, 5000);
+    },
+
+    getMyGroupInfo_Student() {
+      GetMyGroupInfo_Student({
+        task_id: this.task_id
+      }).then(({ live_room_sys_user_id, room_id, student_list }) => {
+        let data = student_list.find(el => {
+          return el.is_self === 'true';
+        });
+        this.$router.push({
+          path: '/live/group',
+          query: {
+            task_id: this.task_id,
+            live_room_sys_user_id,
+            room_id,
+            sessionid: data.session_id
+          }
+        });
+      });
+    },
+
     // 弹出框方法
     dialogMaterialClose() {
       this.dialogVisibleMaterial = false;
@@ -455,6 +470,7 @@ $live-bc: #3d3938;
       background-color: #fff;
       border-radius: 8px;
 
+      // 等待接通
       .loading {
         position: relative;
         width: 100%;
@@ -484,6 +500,7 @@ $live-bc: #3d3938;
         }
       }
 
+      // 连线
       .student-parent {
         position: relative;
         width: 100%;
@@ -492,7 +509,6 @@ $live-bc: #3d3938;
         #student {
           width: 100%;
           height: 468px;
-          border: 1px solid #ccc;
           position: relative;
           background-color: $live-bc;
         }
@@ -529,10 +545,19 @@ $live-bc: #3d3938;
         }
       }
 
+      // 分组讨论
+      .group {
+        width: 100%;
+        height: 468px;
+        position: relative;
+        background-color: $live-bc;
+        overflow: hidden;
+      }
+
+      // 画板
       #draw-parent {
         width: 100%;
         height: 468px;
-        border: 1px solid #ccc;
         position: relative;
         background-color: $live-bc;
         overflow: hidden;

+ 14 - 3
src/views/live/student/live.js

@@ -11,6 +11,14 @@ export {
   handsDown
 } from '@/views/live/common';
 
+// 分组讨论
+
+let r;
+export function init(data) {
+  r = new Rtc(data);
+  return r;
+}
+
 // 连麦
 
 /**
@@ -84,21 +92,20 @@ export function initListener(vue) {
   });
 
   // 新增订阅流事件
-  rtc.on('allow_sub', function (stream) {
+  rtc.on('allow_sub', stream => {
     if (stream.isMixed()) {
       console.log('是混合流,不订阅');
     } else {
       // 订阅远程流
       rtc.trySubscribeStream({
         tryStream: stream,
-        success: function (stream) {
+        success: stream => {
           // 订阅流成功
           let streamType = stream.streamType();
           vue.remoteStreamType = streamType;
           console.log('订阅流成功', streamType);
           let id = streamType === 0 ? 'live' : 'student';
           stream.show(id, 'contain'); // 将流显示到指定 id 的盒子中
-
           if (streamType === 1 || streamType === 10) {
             vue.connect = true;
             vue.callLoading = false;
@@ -165,6 +172,10 @@ export function initListener(vue) {
     // 直播开启状态下,尝试重推这条流
   });
 
+  rtc.on('kick_out', function () {
+    console.log('自己被踢出房间');
+  });
+
   // 视频无法自动播放
   rtc.on('playError', data => {
     console.log('视频无法自动播放', data);

+ 1 - 0
src/views/live/teacher/CompleteList.vue

@@ -4,6 +4,7 @@
     :visible="dialogVisibleComplete"
     width="900px"
     title="答题统计"
+    :close-on-click-modal="false"
     @close="dialogCompleteClose"
   >
     <div class="complete-list-top">

+ 67 - 15
src/views/live/teacher/index.vue

@@ -9,7 +9,7 @@
           <el-button v-if="liveStat" icon="el-icon-switch-button" @click="closeLiveRoom">
             结束直播
           </el-button>
-          <el-button v-if="liveStat" @click="publishStream">推流</el-button>
+          <!-- <el-button v-if="liveStat" @click="publishStream">推流</el-button> -->
         </div>
       </div>
       <div class="live-course-name">{{ roomInfo.course_name }}</div>
@@ -75,17 +75,26 @@
             </span>
           </div>
         </div>
+        <div v-show="isGroup" class="group"></div>
         <div class="button-group">
           <div class="button-group-left">
-            <span title="屏幕共享" @click="publishShareStream">
-              <svg-icon icon-class="share" />
-            </span>
-            <span title="白板" @click="showDrawSetting">
-              <svg-icon icon-class="draw" />
-            </span>
-            <span title="课件推送" @click="dialogVisible = true">
-              <svg-icon icon-class="push" />
-            </span>
+            <template v-if="isGroup">
+              <span class="stop-group" @click="stopGroup">结束群组讨论</span>
+            </template>
+            <template v-else>
+              <span @click="publishShareStream">
+                <svg-icon icon-class="share" />
+              </span>
+              <span @click="showDrawSetting">
+                <svg-icon icon-class="draw" />
+              </span>
+              <span @click="startGroup">
+                <svg-icon icon-class="group" />
+              </span>
+              <span @click="dialogVisible = true">
+                <svg-icon icon-class="push" />
+              </span>
+            </template>
           </div>
           <div class="button-group-right"></div>
         </div>
@@ -174,7 +183,11 @@ import {
   GetLiveRoomStudentList,
   CloseLiveRoom,
   GetLiveRoomInfo,
-  StudentExitLiveRoom
+  StudentExitLiveRoom,
+  StartGroup,
+  StopGroup,
+  GetGroupInfo_Teacher,
+  IsEnableGroup
 } from '@/api/live';
 import SelectMaterial from '@/components/live/SelectMaterial.vue';
 import CompleteList from './CompleteList.vue';
@@ -200,6 +213,7 @@ export default {
       // 定时器
       timer: null,
       remoteStreamType: -1,
+      isGroup: false,
       rtc: null,
       roomData: {
         desc: '直播间标题',
@@ -242,7 +256,7 @@ export default {
   computed: {
     // 画板模式
     isDraw() {
-      return !this.connect && !this.callLoading;
+      return !this.connect && !this.callLoading && !this.isGroup;
     }
   },
   watch: {
@@ -289,6 +303,7 @@ export default {
       true
     );
     this.getLiveRoomStudentListPolling();
+    this.isEnableGroup();
   },
   beforeDestroy() {
     // 清除所有定时器
@@ -492,11 +507,38 @@ export default {
       StudentExitLiveRoom({ task_id: this.task_id, room_user_id });
     },
 
-    // 弹出框方法
     publishStream() {
       common.publishStream('main');
     },
 
+    // 分组讨论
+    startGroup() {
+      StartGroup({ task_id: this.task_id })
+        .then(() => {
+          this.isGroup = true;
+          return GetGroupInfo_Teacher({ task_id: this.task_id });
+        })
+        .then(({ group_list, live_room_sys_user_id }) => {
+          console.log(group_list, live_room_sys_user_id);
+        });
+    },
+
+    stopGroup() {
+      StopGroup({ task_id: this.task_id }).then(() => {
+        this.isGroup = false;
+        this.$message.success('结束分组讨论成功');
+      });
+    },
+
+    isEnableGroup() {
+      IsEnableGroup({ task_id: this.task_id }).then(({ is_enable_group }) => {
+        if (is_enable_group === 'true') {
+          this.isGroup = true;
+        }
+      });
+    },
+
+    // 弹出框方法
     dialogClose() {
       this.dialogVisible = false;
     },
@@ -608,7 +650,6 @@ $live-bc: #3d3938;
         #student {
           width: 100%;
           height: 468px;
-          border: 1px solid #ccc;
           position: relative;
           background-color: $live-bc;
         }
@@ -645,10 +686,17 @@ $live-bc: #3d3938;
         }
       }
 
+      .group {
+        width: 100%;
+        height: 468px;
+        position: relative;
+        background-color: $live-bc;
+        overflow: hidden;
+      }
+
       #draw-parent {
         width: 100%;
         height: 468px;
-        border: 1px solid #ccc;
         position: relative;
         background-color: $live-bc;
         overflow: hidden;
@@ -740,6 +788,10 @@ $live-bc: #3d3938;
         }
 
         &-left {
+          .stop-group {
+            color: #fff;
+          }
+
           > span {
             display: inline-block;
             height: 100%;

+ 48 - 4
src/views/live/teacher/live.js

@@ -1,5 +1,5 @@
 import { Message } from 'element-ui';
-import { rtc, publishStream, createLocalStream } from '@/views/live/common';
+import { rtc, updateMcResult } from '@/views/live/common';
 export {
   initSDK,
   downloadWebSDK,
@@ -16,6 +16,52 @@ export {
 } from '@/views/live/common';
 
 /**
+ * 推送本地流
+ */
+function publishStream(streamName) {
+  rtc.publish({
+    streamName,
+    // 推流成功,更新上麦结果
+    success: stream => {
+      console.log('推流成功', stream);
+      updateMcResult(stream.id(), 1);
+    },
+    fail: str => {
+      // 推流失败,更新上麦结果
+      console.log('推流失败,更新上麦结果', str);
+      updateMcResult('', 0);
+    }
+  });
+}
+
+/**
+ * 创建本地流
+ */
+function createLocalStream() {
+  const createData = {
+    video: true,
+    audio: true
+  };
+  rtc.createLocalStream({
+    streamName: 'main',
+    createData,
+    success: function (stream) {
+      console.log('创建本地流成功', stream);
+      // 创建本地流成功,将流展示到id为 live 的 dom 元素盒子中
+      stream.show('live');
+      publishStream('main'); // 如果需要立即推流,执行 publish 方法
+    },
+    fail: function (data) {
+      // 创建本地流失败,应用层处理
+      Message({
+        type: 'error',
+        message: '创建本地流失败:' + data
+      });
+    }
+  });
+}
+
+/**
  * 初始化监听事件
  */
 export function initListener(vue) {
@@ -68,10 +114,8 @@ export function initListener(vue) {
           // 订阅流成功
           let streamType = stream.streamType();
           vue.remoteStreamType = streamType;
-          // let hasVideo = stream.hasVideo();
           console.log('订阅流成功', streamType);
-          let id = streamType === 0 ? 'live' : 'student';
-          stream.show(id, 'contain'); // 将流显示到指定 id 的盒子中
+          stream.show('student', 'contain'); // 将流显示到指定 id 的盒子中
 
           if (streamType === 1 || streamType === 10) {
             vue.connect = true;