WarningDetail.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899
  1. <template>
  2. <div class="warning-detail">
  3. <div class="warning-detail-head">
  4. <h2>预警详情</h2>
  5. <el-button size="mini" icon="el-icon-arrow-left" @click="goBack"
  6. >返回列表</el-button
  7. >
  8. <!-- <el-button
  9. @click="initSubscribeVideo"
  10. type="primary"
  11. size="mini"
  12. icon="el-icon-arrow-left"
  13. >开始视频</el-button
  14. >
  15. <el-button
  16. @click="closeSubscribeVideo"
  17. type="danger"
  18. size="mini"
  19. icon="el-icon-arrow-left"
  20. >关闭视频</el-button
  21. > -->
  22. </div>
  23. <div class="warning-detail-body">
  24. <div class="detail-body-head">
  25. <div class="detail-body-head-left">
  26. <p>
  27. <i class="icon icon-person"></i>
  28. <span>{{ detailInfo.examStudentName }}</span>
  29. </p>
  30. <p>
  31. <span>证件号:</span><span>{{ detailInfo.identity }}</span>
  32. </p>
  33. <p>
  34. <span>科目(代码):</span
  35. ><span>{{ detailInfo.courseNameCode }}</span>
  36. </p>
  37. </div>
  38. <div class="detail-body-head-right">
  39. <el-button
  40. type="primary"
  41. title="查看上一个"
  42. :disabled="holding"
  43. @click="changeStudent(0)"
  44. >
  45. <i class="icon icon-arrow-left"></i>
  46. </el-button>
  47. <el-button
  48. type="primary"
  49. title="查看下一个"
  50. :disabled="holding"
  51. @click="changeStudent(1)"
  52. >
  53. <i class="icon icon-arrow-right"></i>
  54. </el-button>
  55. </div>
  56. </div>
  57. <div class="warning-detail-main">
  58. <div class="warning-action">
  59. <div class="student-avatar">
  60. <img
  61. :src="detailInfo.basePhotoPath"
  62. :alt="detailInfo.examStudentName"
  63. v-if="detailInfo.basePhotoPath"
  64. />
  65. <div class="avatar-default" v-else>
  66. <i class="el-icon-user-solid"></i>
  67. </div>
  68. <div class="avatar-title">学生底照</div>
  69. </div>
  70. <div class="warning-summary">
  71. <div class="warning-summary-row">
  72. <p class="warning-summary-col">
  73. <i class="icon icon-bell"></i>
  74. <span class="line-name"
  75. >系统预警
  76. <em :class="{ 'color-danger': detailInfo.warningCount > 0 }"
  77. >{{ detailInfo.warningCount }}次</em
  78. ></span
  79. >
  80. </p>
  81. <p class="warning-summary-col">
  82. <i class="icon icon-face"></i>
  83. <span class="line-name"
  84. >陌生人脸
  85. <em>{{ detailInfo.multipleFaceCount }}次</em>
  86. </span>
  87. </p>
  88. </div>
  89. <div class="warning-summary-row">
  90. <p class="warning-summary-col">
  91. <i class="icon icon-info"></i>
  92. <span class="line-name"
  93. >异常处理
  94. <em>{{ detailInfo.exceptionCount }}次</em>
  95. </span>
  96. </p>
  97. <p class="warning-summary-col">
  98. <i class="icon icon-success"></i>
  99. <span class="line-name"
  100. >违纪状态
  101. <em :class="{ 'color-danger': isBreach }">
  102. {{ isBreach ? "违纪" : "正常" }}</em
  103. >
  104. </span>
  105. </p>
  106. </div>
  107. <div class="summary-bg">
  108. <div class="summary-bg-line"></div>
  109. <div class="summary-bg-line"></div>
  110. <div class="summary-bg-spin"></div>
  111. <div class="summary-bg-spin"></div>
  112. <div class="summary-bg-spin"></div>
  113. <div class="summary-bg-spin"></div>
  114. </div>
  115. </div>
  116. <div class="action-list">
  117. <el-button
  118. v-if="actionValid"
  119. icon="icon icon-text-message"
  120. size="mideum"
  121. @click="toSendTextMsg"
  122. >文字提醒</el-button
  123. >
  124. <el-button
  125. v-if="actionValid"
  126. icon="icon icon-record"
  127. size="mideum"
  128. @click="toSendAudioMsg"
  129. >录音提醒</el-button
  130. >
  131. <el-button
  132. v-if="detailInfo.monitorVideoSource && actionValid"
  133. icon="icon icon-call"
  134. size="mideum"
  135. :loading="holding"
  136. @click="answer(0)"
  137. >语音通话</el-button
  138. >
  139. <el-button
  140. v-if="detailInfo.monitorVideoSource && actionValid"
  141. icon="icon icon-media"
  142. size="mideum"
  143. :loading="holding"
  144. @click="answer(1)"
  145. >视频通话</el-button
  146. >
  147. <el-button
  148. icon="icon icon-info-danger"
  149. size="mideum"
  150. @click="toBreach"
  151. >{{ isBreach ? "撤销违纪" : "违纪处理" }}</el-button
  152. >
  153. <el-button
  154. v-if="detailInfo.statusCode !== 'ANSWERING'"
  155. icon="icon icon-paper-danger"
  156. size="mideum"
  157. @click="toFinish"
  158. >强制收卷</el-button
  159. >
  160. </div>
  161. </div>
  162. <div class="warning-content">
  163. <div class="warning-videos">
  164. <div
  165. v-for="item in viewVideos"
  166. :key="item.source"
  167. class="student-video-item"
  168. >
  169. <div class="student-video-container">
  170. <div class="student-video-tips">{{ item.name }}</div>
  171. <flv-media
  172. :ref="item.ref"
  173. :live-url="item.liveUrl"
  174. @muted-change="videoAllMuted"
  175. ></flv-media>
  176. </div>
  177. </div>
  178. </div>
  179. <!-- track -->
  180. <div class="warning-track">
  181. <h3 class="warning-track-title">
  182. <i class="icon icon-track"></i>考试轨迹
  183. </h3>
  184. <div
  185. class="warning-track-item"
  186. v-for="log in detailInfo.examStudentLogList"
  187. :key="log.id"
  188. >
  189. <div
  190. :class="[
  191. 'warning-track-type',
  192. log.viewType === 'common' ? 'type-common' : 'type-exception',
  193. ]"
  194. >
  195. <i :class="['icon', `icon-track-${log.viewType}`]"></i>
  196. </div>
  197. <div class="warning-track-body">
  198. <div class="warning-track-info">
  199. <h3>{{ log.title }}</h3>
  200. <p v-if="log.desc">{{ log.desc }}</p>
  201. <p>
  202. 时间段:
  203. <span v-if="log.startTime">{{ log.startTime }} ~ </span>
  204. <span>{{ log.endTime }}</span>
  205. </p>
  206. <p v-if="log.durationTime">
  207. 持续时长约:{{ log.durationTime }}
  208. </p>
  209. </div>
  210. <ul class="warning-track-media" v-if="log.photos">
  211. <li v-for="(photo, pindex) in log.photos" :key="pindex">
  212. <img :src="photo" @click="toViewImg(photo)" />
  213. </li>
  214. </ul>
  215. </div>
  216. </div>
  217. </div>
  218. </div>
  219. </div>
  220. </div>
  221. <!-- student-breach-dialog -->
  222. <student-breach-dialog
  223. :instance="curDetail"
  224. @modified="breachFinish"
  225. ref="StudentBreachDialog"
  226. ></student-breach-dialog>
  227. <!-- warning-text-message-dialog -->
  228. <warning-text-message-dialog
  229. :record-id="examRecordId"
  230. ref="WarningTextMessageDialog"
  231. ></warning-text-message-dialog>
  232. <!-- audio-record-dialog -->
  233. <audio-record-dialog
  234. :record-id="examRecordId"
  235. ref="AudioRecordDialog"
  236. ></audio-record-dialog>
  237. <!-- image-preview -->
  238. <simple-image-preview
  239. :cur-image="curImage"
  240. ref="SimpleImagePreview"
  241. ></simple-image-preview>
  242. <!-- 通话弹出层 -->
  243. <div
  244. v-if="dialogVisible"
  245. class="communication-dialog"
  246. v-move-ele.prevent.stop
  247. >
  248. <div class="communication-box" v-show="!isWaiting">
  249. <div class="communication-host" id="communication-host"></div>
  250. <div class="communication-guest" id="communication-guest"></div>
  251. <div class="communication-action" @mousedown.stop>
  252. <el-button round type="danger" @click.stop="hangup"
  253. >结束通话</el-button
  254. >
  255. </div>
  256. <div class="communication-info">
  257. <span>持续时长:<second-timer ref="SecondTimer"></second-timer></span>
  258. </div>
  259. </div>
  260. <div class="communication-wait" v-show="isWaiting">
  261. <p class="communication-wait-tips">等待接听…</p>
  262. <div class="communication-wait-avatar">
  263. <img
  264. :src="detailInfo.basePhotoPath"
  265. :alt="detailInfo.examStudentName"
  266. />
  267. </div>
  268. <p class="communication-wait-username">
  269. {{ detailInfo.examStudentName }}
  270. </p>
  271. <div class="communication-wait-action" @mousedown.stop>
  272. <el-button round type="danger" @click="hangup">取消通话</el-button>
  273. </div>
  274. </div>
  275. </div>
  276. </div>
  277. </template>
  278. <script>
  279. import {
  280. checkSystemRequirements,
  281. createClient,
  282. createStream,
  283. } from "@/plugins/trtc";
  284. import {
  285. invigilateDetail,
  286. invigilateFinish,
  287. communicationCalling,
  288. communicationOver,
  289. warningStudentDetail,
  290. getUserMonitorKey,
  291. } from "@/api/invigilation";
  292. import FlvMedia from "../common/FlvMedia";
  293. import StudentBreachDialog from "./StudentBreachDialog";
  294. import WarningTextMessageDialog from "./WarningTextMessageDialog";
  295. import AudioRecordDialog from "./audioRecord/AudioRecordDialog";
  296. import SimpleImagePreview from "@/components/imagePreview/SimpleImagePreview";
  297. import SecondTimer from "../common/SecondTimer";
  298. import {
  299. formatDate,
  300. timeNumberToText,
  301. objTypeOf,
  302. snakeToHump,
  303. } from "@/utils/utils";
  304. import MoveEle from "@/plugins/move-ele";
  305. import { mapState } from "vuex";
  306. const domEmpty = (dom) => {
  307. dom.childNodes.forEach((childNode) => {
  308. dom.removeChild(childNode);
  309. });
  310. };
  311. export default {
  312. name: "warning-detail",
  313. components: {
  314. FlvMedia,
  315. StudentBreachDialog,
  316. WarningTextMessageDialog,
  317. AudioRecordDialog,
  318. SimpleImagePreview,
  319. SecondTimer,
  320. },
  321. directives: { MoveEle },
  322. data() {
  323. return {
  324. examRecordId: this.$route.params.examRecordId,
  325. autoAnswerInfo: null,
  326. detailInfo: {},
  327. curDetail: {},
  328. serialIds: [],
  329. exceptionSummary: [],
  330. viewVideos: [],
  331. viewVideoReady: false,
  332. holding: false,
  333. // communication
  334. userMonitor: {},
  335. client: null,
  336. localStream: null,
  337. dialogVisible: false,
  338. isWaiting: false,
  339. subscribeSetTs: [],
  340. loopRunning: false,
  341. loopSetTs: [],
  342. isHandup: false,
  343. curImage: { imgSrc: "" },
  344. };
  345. },
  346. computed: {
  347. ...mapState("invigilation", ["detailIds", "liveDomains"]),
  348. isBreach() {
  349. return (
  350. !this.detailInfo.breachStatus && this.detailInfo.breachStatus !== null
  351. );
  352. },
  353. actionValid() {
  354. return (
  355. this.detailInfo.statusCode &&
  356. ["FIRST_PREPARE", "ANSWERING"].includes(this.detailInfo.statusCode)
  357. );
  358. },
  359. },
  360. watch: {
  361. $route: {
  362. handler() {
  363. this.initData();
  364. },
  365. },
  366. },
  367. mounted() {
  368. const autoAnswerInfo = window.sessionStorage.getItem("autoAnswerInfo");
  369. this.autoAnswerInfo = autoAnswerInfo ? JSON.parse(autoAnswerInfo) : null;
  370. this.initData();
  371. },
  372. methods: {
  373. async initData() {
  374. this.examRecordId = this.$route.params.examRecordId;
  375. await this.getInvigilateDetail().catch(() => {});
  376. await this.getStudentVideo().catch(() => {});
  377. this.holding = false;
  378. // 学生正在考试,开启定时更新
  379. // 自动更新新增正在候考阶段
  380. if (["FIRST_PREPARE", "ANSWERING"].includes(this.detailInfo.statusCode)) {
  381. this.loopRunning = true;
  382. this.clearLoopSetTs();
  383. this.loopSetTs.push(
  384. setTimeout(() => {
  385. this.timerUpdatePage();
  386. }, 10 * 1000)
  387. );
  388. } else {
  389. this.loopRunning = false;
  390. this.clearLoopSetTs();
  391. }
  392. // 自动应答
  393. if (this.autoAnswerInfo) {
  394. this.autoAnswer();
  395. }
  396. },
  397. clearSubscribeSetTs() {
  398. if (!this.subscribeSetTs.length) return;
  399. this.subscribeSetTs.forEach((sett) => {
  400. clearTimeout(sett);
  401. });
  402. this.subscribeSetTs = [];
  403. },
  404. clearLoopSetTs() {
  405. if (!this.loopSetTs.length) return;
  406. this.loopSetTs.forEach((sett) => {
  407. clearTimeout(sett);
  408. });
  409. this.loopSetTs = [];
  410. },
  411. async timerUpdatePage() {
  412. this.clearLoopSetTs();
  413. if (!this.loopRunning) return;
  414. await this.getInvigilateDetail().catch(() => {});
  415. this.loopSetTs.push(
  416. setTimeout(() => {
  417. this.timerUpdatePage();
  418. }, 10 * 1000)
  419. );
  420. },
  421. async getStudentVideo() {
  422. const res = await warningStudentDetail({
  423. examRecordId: this.examRecordId,
  424. });
  425. const orderSources = [
  426. "CLIENT_CAMERA",
  427. "CLIENT_SCREEN",
  428. "MOBILE_FIRST",
  429. "MOBILE_SECOND",
  430. ];
  431. const sourceNames = {
  432. CLIENT_CAMERA: "电脑摄像头",
  433. CLIENT_SCREEN: "考生屏幕",
  434. MOBILE_FIRST: "手机主机位",
  435. MOBILE_SECOND: "手机辅机位",
  436. };
  437. let records = {};
  438. res.data.data.forEach((item, index) => {
  439. const domain = this.liveDomains[index] || this.liveDomains[0];
  440. item.liveUrl = item.liveUrl
  441. ? `${domain}/live/${item.liveUrl.toLowerCase()}.flv`
  442. : "";
  443. item.name = sourceNames[item.source];
  444. item.ref = snakeToHump(item.source) + "Video";
  445. records[item.source] = item;
  446. });
  447. this.viewVideos = [];
  448. orderSources.forEach((source) => {
  449. if (records[source]) {
  450. this.viewVideos.push(records[source]);
  451. }
  452. });
  453. // 展示所有
  454. // this.viewVideos = orderSources.map((source) => {
  455. // return (
  456. // records[source] || {
  457. // liveUrl: null,
  458. // source,
  459. // name: sourceNames[source],
  460. // ref: snakeToHump(source) + "Video",
  461. // }
  462. // );
  463. // });
  464. this.initSubscribeVideo();
  465. },
  466. async getInvigilateDetail() {
  467. const res = await invigilateDetail(this.examRecordId);
  468. this.detailInfo = res.data.data;
  469. this.detailInfo.examStudentLogList = this.parseStudentLogs(
  470. this.detailInfo.examStudentLogList
  471. );
  472. this.exceptionSummary = this.detailInfo.examStudentLogList
  473. .filter((item) => item.viewType === "warning")
  474. .slice(0, 3);
  475. },
  476. parseStudentLogs(examStudentLogList) {
  477. const statusTypes = {
  478. common: [
  479. "FIRST_START",
  480. "RESUME_START",
  481. "IN_PROCESS",
  482. "PREPARE",
  483. "ANSWERING",
  484. "BREAK_OFF",
  485. "RESUME_PREPARE",
  486. "FINISHED",
  487. "FIRST_PREPARE",
  488. ],
  489. warning: [
  490. "FACE_COUNT_ERROR",
  491. "FACE_COMPARE_ERROR",
  492. "EYE_CLOSE_ERROR",
  493. "LIVENESS_ACTION_ERROR",
  494. "REALNESS",
  495. "NONE",
  496. // exception:
  497. "NET_TIME_OUT",
  498. "MACHING_STOP",
  499. "NET_TIME_BREAK",
  500. "SOFTWARE_STOP",
  501. "POWER_CUT",
  502. ],
  503. };
  504. const transformInfo = {
  505. FIRST_START: "身份识别",
  506. ANSWERING: "进入考试",
  507. RESUME_START: "身份识别",
  508. };
  509. let statusTypeMap = {};
  510. Object.keys(statusTypes).map((k) => {
  511. statusTypes[k].map((item) => {
  512. statusTypeMap[item] = k;
  513. });
  514. });
  515. const logs = examStudentLogList.map((item) => {
  516. let info = { ...item };
  517. info.endTime = formatDate("HH:mm:ss", new Date(info.createTime));
  518. info.viewType = statusTypeMap[info.type] || "common";
  519. const content = info.info.split(/【|】/);
  520. if (content.length === 3) {
  521. info.title = content[1];
  522. info.desc = content[2];
  523. } else {
  524. info.title = transformInfo[info.type] || content[0];
  525. }
  526. if (info.remark && info.remark.includes('{"')) {
  527. info.remark = JSON.parse(info.remark);
  528. if (info.remark["MIN_CREATE_TIME"]) {
  529. info.startTime = formatDate(
  530. "HH:mm:ss",
  531. new Date(info.remark["MIN_CREATE_TIME"])
  532. );
  533. info.durationTime = timeNumberToText(
  534. info.createTime - info.remark["MIN_CREATE_TIME"]
  535. );
  536. }
  537. let photos = [];
  538. Object.keys(info.remark).map((key) => {
  539. if (key.includes("PHOTO")) {
  540. const kPhotos =
  541. objTypeOf(info.remark[key]) === "array"
  542. ? info.remark[key]
  543. : [info.remark[key]];
  544. photos = [...photos, ...kPhotos];
  545. }
  546. });
  547. info.photos = photos;
  548. } else if (info.updateTime) {
  549. info.startTime = formatDate("HH:mm:ss", new Date(info.createTime));
  550. info.endTime = formatDate("HH:mm:ss", new Date(info.updateTime));
  551. info.durationTime = timeNumberToText(
  552. info.updateTime - info.createTime
  553. );
  554. }
  555. return info;
  556. });
  557. return logs;
  558. },
  559. changeStudent(type) {
  560. let index = this.detailIds.indexOf(this.examRecordId);
  561. if (type) {
  562. if (index >= this.detailIds.length - 1) {
  563. this.$message.error("当前没有下一个学生了");
  564. return;
  565. }
  566. index++;
  567. } else {
  568. if (index <= 0) {
  569. this.$message.error("当前没有上一个学生了");
  570. return;
  571. }
  572. index--;
  573. }
  574. if (this.holding) return;
  575. this.holding = true;
  576. this.closeSubscribeVideo();
  577. console.log(this.detailIds[index]);
  578. this.$router.replace({
  579. name: "WarningDetail",
  580. params: {
  581. examRecordId: this.detailIds[index],
  582. },
  583. });
  584. },
  585. toBreach() {
  586. this.curDetail = {
  587. examStudentName: this.detailInfo.examStudentName,
  588. identity: this.detailInfo.identity,
  589. courseNameCode: this.detailInfo.courseNameCode,
  590. description: "",
  591. examRecordId: [this.detailInfo.examRecordId],
  592. breachStatus: this.detailInfo.breachStatus,
  593. status: this.detailInfo.breachStatus ? 0 : 1,
  594. // 状态,0:新建,1:撤销
  595. // 违纪状态:正常(1)=>新建,违纪(0)=>撤销
  596. type: "",
  597. };
  598. this.$refs.StudentBreachDialog.open();
  599. },
  600. async toFinish() {
  601. const result = await this.$confirm(
  602. "试卷若被强制回收,考试再无法重置继续完成考试,请您慎重选择!您确定要强制回收改考试试卷吗?",
  603. "强制收卷确认提醒",
  604. {
  605. confirmButtonText: "确定",
  606. cancelButtonText: "取消",
  607. iconClass: "el-icon-warning",
  608. customClass: "el-message-box__error",
  609. }
  610. ).catch(() => {});
  611. if (!result) return;
  612. await invigilateFinish({
  613. examRecordId: [this.detailInfo.examRecordId],
  614. type: "INTERRUPT",
  615. });
  616. this.$message.success("强制收卷成功!");
  617. this.goBack();
  618. },
  619. toSendTextMsg() {
  620. this.$refs.WarningTextMessageDialog.open();
  621. },
  622. toSendAudioMsg() {
  623. this.$refs.AudioRecordDialog.open();
  624. },
  625. breachFinish() {
  626. this.getInvigilateDetail();
  627. },
  628. // video relative
  629. notifyError(content) {
  630. this.$notify({
  631. type: "error",
  632. message: content,
  633. });
  634. },
  635. async initClient(examRecordId) {
  636. const res = await getUserMonitorKey(examRecordId);
  637. this.userMonitor = res.data.data;
  638. this.client = createClient({
  639. mode: "live",
  640. sdkAppId: this.userMonitor.appId * 1,
  641. userId: this.userMonitor.monitorUserId,
  642. userSig: this.userMonitor.monitorUserSig,
  643. useStringRoomId: true,
  644. });
  645. },
  646. async getLocalMedia(isVideo) {
  647. const localStream = createStream({
  648. userId: this.userMonitor.monitorUserId,
  649. audio: true,
  650. video: !!isVideo,
  651. });
  652. const errorTips = {
  653. NotFoundError: "找不到硬件设备,请确保硬件设备正常。",
  654. NotAllowedError: "不授权摄像头/麦克风访问无法进行音视频通话。",
  655. NotReadableError:
  656. "暂时无法访问摄像头/麦克风,请确保当前没有其他应用请求访问摄像头/麦克风,并重试。",
  657. OverConstrainedError: "设备异常",
  658. AbortError: "设备异常",
  659. };
  660. let initLocalStreamResult = true;
  661. await localStream.initialize().catch((error) => {
  662. console.log(errorTips[error.name]);
  663. this.notifyError(errorTips[error.name] || "未知错误");
  664. initLocalStreamResult = false;
  665. localStream.close();
  666. });
  667. return initLocalStreamResult && localStream;
  668. },
  669. async autoAnswer() {
  670. await this.answer(this.autoAnswerInfo.isVideo);
  671. // 更改学生的通话申请状态
  672. await communicationCalling({
  673. recordId: this.examRecordId,
  674. source: this.autoAnswerInfo.source,
  675. });
  676. },
  677. async answer(isVideo) {
  678. const result = await checkSystemRequirements().catch(() => {
  679. this.$message.error(
  680. `您的浏览器不支持当前音视频通讯版本。建议使用最新版的chrome浏览器!`
  681. );
  682. });
  683. if (!result) return;
  684. // 客户端两路视频公用一个userId:
  685. // main:有音频,有视频
  686. // auxiliary:无音频,有视频
  687. // 手机端userId各不同
  688. if (this.holding) return;
  689. this.holding = true;
  690. this.videoAllMuted();
  691. await this.initClient(this.examRecordId).catch(() => {});
  692. if (!this.client) {
  693. this.holding = false;
  694. return;
  695. }
  696. this.localStream = await this.getLocalMedia(isVideo);
  697. if (!this.localStream) {
  698. this.holding = false;
  699. return;
  700. }
  701. this.dialogVisible = true;
  702. this.holding = false;
  703. // 添加远程用户视频发布监听
  704. this.client.on("stream-added", (event) => {
  705. console.log(event);
  706. console.log(event.stream.getUserId(), this.userMonitor.sourceUserId);
  707. const remoteStream = event.stream;
  708. if (remoteStream.getUserId() !== this.userMonitor.sourceUserId) return;
  709. if (remoteStream.getType() !== "main") return;
  710. console.log(`有效视频${remoteStream.getUserId()},准备订阅`);
  711. if (this.autoAnswerInfo) {
  712. // 存在自动应答信息时,不再延迟订阅学生音视频流
  713. this.client
  714. .subscribe(remoteStream, { audio: true, video: true })
  715. .catch((error) => {
  716. console.log(`${remoteStream.getUserId()}视频订阅失败!`, error);
  717. this.notifyError("学生视频获取失败!");
  718. });
  719. } else {
  720. // 延迟订阅视频
  721. this.subscribeSetTs.push(
  722. setTimeout(() => {
  723. this.client
  724. .subscribe(remoteStream, { audio: true, video: true })
  725. .catch((error) => {
  726. console.log(
  727. `${remoteStream.getUserId()}视频订阅失败!`,
  728. error
  729. );
  730. this.notifyError("学生视频获取失败!");
  731. });
  732. }, 5000)
  733. );
  734. }
  735. });
  736. this.client.on("stream-subscribed", (event) => {
  737. const remoteStream = event.stream;
  738. console.log(event);
  739. console.log(`${remoteStream.getUserId()}视频已订阅!`);
  740. this.isWaiting = false;
  741. this.$nextTick(() => {
  742. if (!this.$refs.SecondTimer.recoding) this.$refs.SecondTimer.start();
  743. domEmpty(document.getElementById("communication-host"));
  744. remoteStream.play("communication-host", { objectFit: "contain" });
  745. });
  746. });
  747. this.client.on("stream-removed", (event) => {
  748. const remoteStream = event.stream;
  749. if (
  750. remoteStream.getUserId() !== this.userMonitor.sourceUserId ||
  751. remoteStream.getType() !== "main" ||
  752. this.isHandup
  753. )
  754. return;
  755. console.log(event);
  756. console.log(`${remoteStream.getUserId()}已退出房间!`);
  757. this.notifyError("对方已挂断!");
  758. this.hangup();
  759. });
  760. // 加入房间
  761. let roomJoinResult = true;
  762. await this.client
  763. .join({
  764. roomId: this.userMonitor.monitorKey,
  765. role: "audience",
  766. })
  767. .catch((error) => {
  768. roomJoinResult = false;
  769. console.log("加入房间失败!", error);
  770. this.notifyError("发起通信失败!");
  771. });
  772. if (!roomJoinResult) return;
  773. console.log("加入房间成功!");
  774. // 切换角色,连麦互动
  775. let switchResult = true;
  776. await this.client.switchRole("anchor").catch((error) => {
  777. console.log("切换角色失败!", error);
  778. this.notifyError("角色错误!");
  779. switchResult = false;
  780. });
  781. if (!switchResult) return;
  782. // 发布本地视频
  783. let publishStreamResult = true;
  784. this.client.publish(this.localStream).catch((error) => {
  785. console.log("发布本地视频失败!", error);
  786. this.notifyError("本地音视频推送失败!");
  787. publishStreamResult = false;
  788. });
  789. if (!publishStreamResult) return;
  790. console.log("发布本地音视频成功!");
  791. // 播放本地视频
  792. this.localStream.play("communication-guest", { muted: true });
  793. this.isHandup = false;
  794. },
  795. async hangup() {
  796. if (this.isHandup) return;
  797. this.isHandup = true;
  798. this.clearSubscribeSetTs();
  799. this.$refs.SecondTimer.end();
  800. // 取消发布本地视频
  801. await this.client.unpublish(this.localStream).catch((error) => {
  802. console.log("取消发布本地视频失败!", error);
  803. });
  804. this.localStream.close();
  805. this.localStream = null;
  806. // 离开房间
  807. let result = true;
  808. await this.client.leave().catch((error) => {
  809. console.log("离开房间失败!", error);
  810. this.notifyError("操作异常,请重新尝试!");
  811. result = false;
  812. });
  813. if (!result) return;
  814. this.client.off("*");
  815. this.client = null;
  816. this.userMonitor = {};
  817. this.dialogVisible = false;
  818. this.isWaiting = true;
  819. // this.initSubscribeVideo();
  820. if (this.autoAnswerInfo) {
  821. // 结束学生的通话
  822. await communicationOver({
  823. recordId: this.examRecordId,
  824. source: this.autoAnswerInfo.source,
  825. }).catch(() => {
  826. console.log("结束通话状态异常!");
  827. });
  828. this.goBack();
  829. }
  830. },
  831. initSubscribeVideo() {
  832. this.viewVideoReady = true;
  833. },
  834. closeSubscribeVideo() {
  835. this.viewVideoReady = false;
  836. },
  837. videoAllMuted() {
  838. this.viewVideos
  839. .filter((vv) => vv.liveUrl)
  840. .forEach((vv) => {
  841. this.$refs[vv.ref][0].mutedPlayer(true);
  842. });
  843. },
  844. toViewImg(photo) {
  845. this.curImage = { imgSrc: photo };
  846. this.$refs.SimpleImagePreview.open();
  847. },
  848. goBack() {
  849. window.history.go(-1);
  850. },
  851. },
  852. beforeDestroy() {
  853. window.sessionStorage.removeItem("autoAnswerInfo");
  854. this.loopRunning = false;
  855. this.clearLoopSetTs();
  856. this.clearSubscribeSetTs();
  857. if (this.client) {
  858. this.client.leave();
  859. this.client.off("*");
  860. }
  861. },
  862. };
  863. </script>