iMedia.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. <template>
  2. <div>
  3. <h2 class="tc">
  4. <span>{{curTimes|useTime}}</span>
  5. <strong>/</strong>
  6. <span>{{media.duration|useTime}}</span>
  7. </h2>
  8. <el-row style="width: 1100px;">
  9. <el-col :span="18" style="width:680px;">
  10. <div style="height: 540px;">
  11. <video-player id="myVideo" class="video-player-box" ref="videoPlayer"
  12. @pause="onPlayerPause($event)"
  13. @play="onPlayerStart($event)"
  14. @ready="playerReadied"
  15. @timeupdate="onPlayerTimeupdate($event)" @ended="onPlayerEnded($event)" :globalOptions="{controls:true}"
  16. :options="options">
  17. </video-player>
  18. </div>
  19. <div class="tc">
  20. <p v-if="errMsg" style="font-size: 30px;color: red;"> {{errMsg}}</p>
  21. </div>
  22. </el-col>
  23. <el-col :span="6" style="width: 420px;float:right;">
  24. <div class="account-tit2">
  25. <a :class="{'current':required===1}" @click="required=1" style="width: 100px;">必修课程</a>
  26. <a :class="{'current':required===0}" @click="required=0" style="width: 100px;">选修课程</a>
  27. <a :class="{'current':required===2}" @click="required=2" style="width: 100px;">讨论区</a>
  28. </div>
  29. <el-menu v-if="required==1" style="width: 400px;font-size: 10px;">
  30. <ul class="m-chapter-list">
  31. <li v-for="(item, index) in chapter.required" :key="index" :class="{'current':item.name==activeChapter}">
  32. <a href="javascript:void(0)" style="text-decoration: none" @click="goState(item)" class="ng-binding">
  33. {{parseInt(item.xs/10)}}学时 {{item.name}}</a>
  34. <div class="sub-list" v-if="item.name==activeChapter">
  35. <a v-for="(subItem,index) in list" :key="subItem.id" v-if="subItem.chapterName == activeChapter"
  36. @click="goSubState(subItem, index)" :class="{'current':subItem.name==activeName}">
  37. <span class="media-process">
  38. <el-progress :percentage="subItem.percent" type="circle" :width="16" :height="16"
  39. :format="()=>{return ''}" v-if="subItem.percent>=100" color="green"></el-progress>
  40. <el-progress :percentage="subItem.percent" type="circle" :width="16" :height="16"
  41. :format="()=>{return ''}" v-else-if="subItem.percent>=50" color="cyan"></el-progress>
  42. <el-progress :percentage="subItem.percent" type="circle" :width="16" :height="16"
  43. :format="()=>{return ''}" v-else></el-progress>
  44. </span>
  45. <span class="media-name"> {{subItem.name}} </span>
  46. </a>
  47. </div>
  48. </li>
  49. </ul>
  50. </el-menu>
  51. <el-menu v-if="required==0">
  52. <ul class="m-chapter-list" style="width: 400px;font-size: 10px;">
  53. <li v-for="(item, index) in chapter.normal" :key="index" :class="{'current':item.name==activeChapter}">
  54. <a href="javascript:void(0)" style="text-decoration: none" @click="goState(item)"
  55. class="ng-binding">{{parseInt(item.xs/10)}}学时 {{item.name}}</a>
  56. <div class="sub-list pt10" v-if="item.name==activeChapter">
  57. <a v-for="(subItem,index) in list" :key="subItem.id" v-if="subItem.chapterName == activeChapter"
  58. @click="goSubState(subItem, index)" :class="{'current':subItem.name==activeName}">
  59. <span class="media-process">
  60. <el-progress :percentage="subItem.percent" type="circle" :width="16" :height="16"
  61. :format="()=>{return ''}" v-if="subItem.percent>=100" color="green"></el-progress>
  62. <el-progress :percentage="subItem.percent" type="circle" :width="16" :height="16"
  63. :format="()=>{return ''}" v-else-if="subItem.percent>=50" color="cyan"></el-progress>
  64. <el-progress :percentage="subItem.percent" type="circle" :width="16" :height="16"
  65. :format="()=>{return ''}" v-else></el-progress>
  66. </span>
  67. <span class="media-name"> {{subItem.name}} </span>
  68. </a>
  69. </a>
  70. </div>
  71. </li>
  72. </ul>
  73. <!-- <el-menu-item v-for="item in chapter.normal" :key="item">{{item}}</el-menu-item> -->
  74. </el-menu>
  75. <div v-if="required==2">
  76. <i-message :media-id="media.mediaId"></i-message>
  77. </div>
  78. </el-col>
  79. </el-row>
  80. <div class="left-float" v-if="!closeFace" v-drag v-show="identifyFacePass">
  81. <video ref="video" width="240" height="180" autoplay></video>
  82. <canvas ref="canvas" v-show="ontakebtn" width="240" height="180"></canvas>
  83. </div>
  84. <el-dialog title="人脸认证" center :visible.sync="identifyFace" width="500px" :close-on-click-modal="false">
  85. <div style="width: 240px;margin: 100px auto;">
  86. <video ref="video2" width="240" height="180" autoplay></video>
  87. <p style="margin-top: 20px;">当前照片:</p>
  88. <canvas ref="canvas" width="240" height="180"></canvas>
  89. <p v-if="errMsg" style="font-size: 30px;color: red;"> {{errMsg}}</p>
  90. </div>
  91. </el-dialog>
  92. </div>
  93. </template>
  94. <script>
  95. // import tracking from '@/assets/tracking/build/tracking-min.js';
  96. // import '@/assets/tracking/build/data/face-min.js';
  97. import {
  98. httpServer
  99. } from "@/components/httpServer/httpServer.js";
  100. import md5 from 'js-md5';
  101. import {
  102. videoPlayer
  103. } from 'vue-video-player';
  104. import IMessage from './iMessage.vue'
  105. import 'video.js/dist/video-js.css'
  106. // import html2canvas from "html2canvas";
  107. import {
  108. MessageBox
  109. } from "element-ui";
  110. export default {
  111. name: "Index",
  112. data() {
  113. return {
  114. timer: false,
  115. required: 1,
  116. tickNum: 0,
  117. prevTime: 0,
  118. isReady: false,
  119. ontakebtn: false,
  120. identifyFace: false,
  121. identifyFacePass: false,
  122. activeChapter: '',
  123. activeName: '',
  124. curTimes: 0,
  125. errMsg: '',
  126. errCount: 0,
  127. onPlay: false
  128. }
  129. },
  130. components: {
  131. videoPlayer,
  132. IMessage
  133. },
  134. props: ['media', 'options', 'heartbeat', 'maxErrorCount', 'collectBeat', 'closeFace', 'chapter', 'list'],
  135. filters: {
  136. useTime(val) {
  137. let timestr = ""
  138. let hour = parseInt(val / 3600);
  139. let min = parseInt(val / 60 % 60);
  140. let sec = parseInt(val % 60);
  141. if (hour < 10) hour = "0" + hour;
  142. if (min < 10) min = "0" + min;
  143. if (sec < 10) sec = "0" + sec;
  144. return hour + ":" + min + ":" + sec
  145. }
  146. },
  147. beforeDestroy() {
  148. this.stopTick()
  149. this.reportErr("play", 'destroy');
  150. },
  151. directives: {
  152. drag(el, bindings) {
  153. el.onmousedown = function(e) {
  154. var disx = e.pageX - el.offsetLeft
  155. var disy = e.pageY - el.offsetTop
  156. document.onmousemove = function(e) {
  157. el.style.left = e.pageX - disx + 'px'
  158. el.style.top = e.pageY - disy + 'px'
  159. }
  160. document.onmouseup = function() {
  161. document.onmousemove = document.onmouseup = null
  162. }
  163. }
  164. }
  165. },
  166. computed: {
  167. player() {
  168. return this.$refs.videoPlayer.player
  169. }
  170. },
  171. beforeDestroy() {
  172. this.destroyTimer()
  173. this.closeCamera( "video" )
  174. },
  175. created() {
  176. this.startTick()
  177. this.startMonitor();
  178. this.activeChapter = this.media.chapterName;
  179. this.activeName = this.media.name
  180. },
  181. watch: {
  182. "media.name"() {
  183. this.activeChapter = this.media.chapterName;
  184. this.activeName = this.media.name
  185. this.tickNum = 0
  186. this.errMsg = ''
  187. this.errCount = 0
  188. this.startIdentify()
  189. this.setposition( this.media.position )
  190. }
  191. },
  192. methods: {
  193. photograph( ref ) {
  194. let identify = (ref=="video2");
  195. let ctx = this.$refs["canvas"].getContext("2d");
  196. // 把当前视频帧内容渲染到canvas上
  197. this.ontakebtn = true
  198. ctx.drawImage(this.$refs[ref], 0, 0, 240, 180);
  199. // 转base64格式、图片格式转换、图片质量压缩
  200. let imgBase64 = this.$refs["canvas"].toDataURL("image/jpeg", 1); // 由字节转换为KB 判断大小
  201. this.ontakebtn = false
  202. let str = imgBase64.replace("data:image/jpeg;base64,", "");
  203. let param = {
  204. id: this.media.id,
  205. ref,
  206. image: str
  207. }
  208. httpServer("course.collect", param).then(res => {
  209. let {
  210. msg,
  211. pause
  212. } = res.data
  213. this.errMsg = msg || '';
  214. if (msg) {
  215. this.errCount++
  216. } else {
  217. if( identify ){
  218. this.identifyPassAndPlay()
  219. }
  220. this.errCount = 0;
  221. }
  222. if (!identify && this.errCount > this.maxErrorCount) {
  223. this.doPause();
  224. }
  225. })
  226. },
  227. destroyTimer() {
  228. if (this.timer) clearInterval(this.timer);
  229. },
  230. goState(item) {
  231. if (item.name == this.activeChapter) {
  232. this.activeChapter = ""
  233. } else {
  234. this.activeChapter = item.name;
  235. }
  236. },
  237. goSubState(item, index) {
  238. this.$emit('loadMedia', item, index)
  239. },
  240. callCamera( ref ) {
  241. // H5调用电脑摄像头API
  242. if (this.closeFace) {
  243. this.identifyFacePass = true;
  244. return;
  245. }
  246. navigator.mediaDevices
  247. .getUserMedia({
  248. video: true,
  249. })
  250. .then((success) => {
  251. // 摄像头开启成功
  252. this.$refs[ref].srcObject = success;
  253. // 实时拍照效果
  254. this.$refs[ref].play();
  255. })
  256. .catch((error) => {
  257. this.$message.error(
  258. "摄像头开启失败,请检查摄像头是否可用!或者打开摄影头"
  259. );
  260. console.error("摄像头开启失败,请检查摄像头是否可用!");
  261. });
  262. },
  263. closeCamera( ref ) {
  264. if (!this.$refs[ref]) return;
  265. if (!this.$refs[ref].srcObject) return;
  266. let stream = this.$refs[ref].srcObject;
  267. let tracks = stream.getTracks();
  268. tracks.forEach((track) => {
  269. track.stop();
  270. });
  271. this.$refs[ref].srcObject = null;
  272. },
  273. startTick() {
  274. let tick = this.tryTick;
  275. this.destroyTimer();
  276. this.tickNum = 0;
  277. this.timer = setTimeout(tick, 1 * 1000);
  278. },
  279. stopTick() {
  280. if (this.timer) clearInterval(this.timer);
  281. },
  282. tryTick() {
  283. let that = this;
  284. try {
  285. that.tick()
  286. } catch (err) {
  287. that.reportErr("play", '' + err.message)
  288. }
  289. this.destroyTimer()
  290. this.timer = setTimeout(this.tryTick, 1 * 1000);
  291. },
  292. playerReadied(audio) {
  293. let that = this;
  294. let {
  295. position,
  296. duration
  297. } = this.media
  298. if (position > 5 && position < duration) {
  299. setTimeout(() => {
  300. this.setposition(position)
  301. }, 2000)
  302. }
  303. this.isReady = true
  304. },
  305. onPlayerTimeupdate(player) {
  306. let curTimes = player.cache_.currentTime;
  307. if (curTimes > 30 && curTimes > this.curTimes + 2) {
  308. console.log("return", curTimes, this.media.position)
  309. player.currentTime(this.curTimes);
  310. return;
  311. }
  312. this.curTimes = curTimes
  313. },
  314. setposition(position) {
  315. if (position > this.media.duration) position = this.media.duration;
  316. let player = this.$refs.videoPlayer.player;
  317. let res = player.currentTime(position);
  318. console.log("setposition", position)
  319. // player.play()
  320. this.curTimes = position;
  321. if (this.media.isFinish) return;
  322. if (this.media.position >= this.media.duration - 2*this.heartbeat && !this.media.isFinish) {
  323. this.tick(true)
  324. }
  325. },
  326. onPlayerPause(event) {
  327. this.reportErr("play", 'pause');
  328. this.onPlay = false
  329. },
  330. onPlayerEnded(event) {
  331. this.reportErr("play", 'end');
  332. this.tick(true)
  333. },
  334. onClose() {
  335. this.reportErr("play", 'close')
  336. this.doPause()
  337. this.$emit("close")
  338. this.closeCamera( "video")
  339. },
  340. doPause() {
  341. this.onPlay = false
  342. let myPlayer = this.$refs.videoPlayer.player;
  343. myPlayer && myPlayer.pause()
  344. },
  345. doPlay() {
  346. this.onPlay = true
  347. this.startTick();
  348. if (!this.$refs.videoPlayer || !this.$refs.videoPlayer.player) return;
  349. // if (!this.dialog) return this.doPause();
  350. let myPlayer = this.$refs.videoPlayer.player;
  351. myPlayer && myPlayer.play()
  352. this.tickNum = 0
  353. },
  354. onPlayerStart( player ) {
  355. console.log("onPlayerStart")
  356. this.onPlay = true
  357. if( !this.identifyFacePass){
  358. this.startIdentify()
  359. }
  360. this.reportErr("play", 'start');
  361. this.startTick();
  362. },
  363. startIdentify(){
  364. this.identifyFace = true
  365. this.identifyFacePass = false
  366. this.closeCamera("video")
  367. this.callCamera("video2")
  368. this.startTick()
  369. },
  370. identifyPassAndPlay(){
  371. this.identifyFacePass = true
  372. this.identifyFace = false;
  373. this.closeCamera("video2")
  374. this.callCamera("video")
  375. this.$message.successMsg("人脸认证通过", 2)
  376. this.doPlay()
  377. },
  378. reportErr(action, msg) {
  379. httpServer("course.report", {
  380. action,
  381. msg
  382. })
  383. },
  384. startMonitor() {
  385. let that = this
  386. document.addEventListener("visibilitychange", function() {
  387. // || document.hidden
  388. if (document.visibilityState == "hidden") {
  389. // that.doPause( )
  390. that.reportErr("play", 'hidden');
  391. } else {
  392. that.reportErr("play", 'show');
  393. // that.doPlay()
  394. }
  395. });
  396. },
  397. tick(force = false) {
  398. let media = this.media;
  399. this.tickNum++
  400. // 人脸认证期间
  401. if( !this.identifyFacePass ) {
  402. if( this.onPlay ){
  403. this.doPause()
  404. }
  405. // 人脸认证
  406. if (this.tickNum % 3 == 1) {
  407. this.photograph( "video2" );
  408. }
  409. return;
  410. };
  411. // 未开始
  412. if(!force && !this.onPlay ){
  413. return;
  414. }
  415. // 已经完成
  416. if (this.media.isFinish) {
  417. console.log("finish")
  418. return;
  419. }
  420. // 每5秒一次心跳
  421. if (this.tickNum % this.heartbeat != 0) {
  422. return;
  423. }
  424. // if (!this.media.isFinish && this.onPlay && !this.closeFace) {
  425. // console.log(this.identifyFacePass , this.media.isFinish , this.onPlay, this.closeFace)
  426. // this.$message.errorMsg("需要安装摄像头才能学习", 2);
  427. // this.doPause()
  428. // return;
  429. // }
  430. if (this.errCount >= this.maxErrorCount) {
  431. this.$message.errorMsg("人脸不在摄像头上", 5);
  432. this.destroyTimer()
  433. this.$emit("close");
  434. return;
  435. }
  436. let heartBeat = parseInt(this.tickNum / this.heartbeat);
  437. // 异常 10秒检查
  438. if (!this.closeFace) {
  439. if (this.errCount > 0) {
  440. this.photograph("video")
  441. } else if (heartBeat % this.collectBeat == 1) {
  442. this.photograph("video")
  443. }
  444. }
  445. // 主动暂停
  446. let myPlayer = this.$refs.videoPlayer.player;
  447. let curTimes = parseInt(myPlayer.currentTime());
  448. // 后退无心跳
  449. if( !force ){
  450. if (curTimes < this.media.position+this.heartbeat) {
  451. return;
  452. }
  453. }
  454. let isFinish = force ? 1 : 0
  455. if (curTimes >= media.duration) isFinish = 1;
  456. // 拉到后面
  457. if (!isFinish) {
  458. if (!this.onPlay) return;
  459. }
  460. // 强制完成
  461. let param = {
  462. id: media.id,
  463. position: curTimes,
  464. isFinish
  465. };
  466. httpServer("course.tick", param, true).then(res => {
  467. if (res.code == 200) {
  468. let {
  469. skip,
  470. position,
  471. pause,
  472. closed
  473. } = res.data
  474. if (pause || closed) {
  475. this.doPause();
  476. this.$emit("close")
  477. if (closed) {
  478. this.$message.errorMsg("课程关闭学习", 5);
  479. } else if (pause) {
  480. this.$message.errorMsg("多处同时播放视频", 5);
  481. }
  482. return
  483. }
  484. if (!skip) {
  485. setTimeout(() => {
  486. this.setposition(position)
  487. }, 2000);
  488. };
  489. Object.assign(param, res.data)
  490. this.$emit("update", param)
  491. }
  492. })
  493. }
  494. }
  495. }
  496. </script>
  497. <style>
  498. @import url("./imedia.css");
  499. .video-js {
  500. .vjs-control-bar {
  501. .vjs-icon-custombutton {
  502. font-family: VideoJS;
  503. font-weight: normal;
  504. font-style: normal;
  505. }
  506. .vjs-icon-custombutton:before {
  507. content: "\f108";
  508. font-size: 1.8em;
  509. line-height: 1.67;
  510. }
  511. }
  512. }
  513. .left-float {
  514. width: 240px;
  515. height: 180px;
  516. background-color: #8bbdf5;
  517. position: fixed;
  518. transition: bottom ease .9s;
  519. z-index: 0;
  520. left: 60px;
  521. top: 120px;
  522. text-align: center;
  523. border-radius: 5px;
  524. }
  525. .p-process {
  526. width: 100%;
  527. margin: 20px auto;
  528. height: 30px;
  529. }
  530. .media-footer {
  531. padding: 0px 30px;
  532. text-align: left;
  533. line-height: 40px !important;
  534. bottom: -10px;
  535. }
  536. .media-center {
  537. text-align: center;
  538. padding: 0px;
  539. }
  540. .media-time {
  541. font-size: 18px;
  542. vertical-align: center;
  543. }
  544. .media-select {
  545. white-space: nowrap;
  546. text-align: right;
  547. line-height: 40px !important;
  548. float: right;
  549. margin: 0px !important;
  550. }
  551. .bicon {
  552. font-size: 28px !important;
  553. padding: 4px !important;
  554. }
  555. .media-el-select {
  556. font-size: 28px !important;
  557. width: 80px;
  558. padding: -4px auto !important;
  559. }
  560. .media-name {
  561. width: 300px;
  562. margin-left: 16px;
  563. line-height: 24px;
  564. white-space: nowrap;
  565. overflow: hidden;
  566. align-items: center;
  567. text-overflow: ellipsis;
  568. }
  569. .media-process{
  570. width: 8px;
  571. height: 8px;
  572. align-items: center;
  573. }
  574. .vjs-tech {
  575. pointer-events: none;
  576. }
  577. </style>