123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609 |
- <template>
- <div>
- <h2 class="tc">
- <span>{{curTimes|useTime}}</span>
- <strong>/</strong>
- <span>{{media.duration|useTime}}</span>
- </h2>
- <el-row style="width: 1100px;">
- <el-col :span="18" style="width:680px;">
- <div style="height: 540px;">
- <video-player id="myVideo" class="video-player-box" ref="videoPlayer"
- @pause="onPlayerPause($event)"
- @play="onPlayerStart($event)"
- @ready="playerReadied"
- @timeupdate="onPlayerTimeupdate($event)" @ended="onPlayerEnded($event)" :globalOptions="{controls:true}"
- :options="options">
- </video-player>
- </div>
- <div class="tc">
- <p v-if="errMsg" style="font-size: 30px;color: red;"> {{errMsg}}</p>
- </div>
- </el-col>
- <el-col :span="6" style="width: 420px;float:right;">
- <div class="account-tit2">
- <a :class="{'current':required===1}" @click="required=1" style="width: 100px;">必修课程</a>
- <a :class="{'current':required===0}" @click="required=0" style="width: 100px;">选修课程</a>
- <a :class="{'current':required===2}" @click="required=2" style="width: 100px;">讨论区</a>
- </div>
- <el-menu v-if="required==1" style="width: 400px;font-size: 10px;">
- <ul class="m-chapter-list">
- <li v-for="(item, index) in chapter.required" :key="index" :class="{'current':item.name==activeChapter}">
- <a href="javascript:void(0)" style="text-decoration: none" @click="goState(item)" class="ng-binding">
- {{parseInt(item.xs/10)}}学时 {{item.name}}</a>
- <div class="sub-list" v-if="item.name==activeChapter">
- <a v-for="(subItem,index) in list" :key="subItem.id" v-if="subItem.chapterName == activeChapter"
- @click="goSubState(subItem, index)" :class="{'current':subItem.name==activeName}">
- <span class="media-process">
- <el-progress :percentage="subItem.percent" type="circle" :width="16" :height="16"
- :format="()=>{return ''}" v-if="subItem.percent>=100" color="green"></el-progress>
- <el-progress :percentage="subItem.percent" type="circle" :width="16" :height="16"
- :format="()=>{return ''}" v-else-if="subItem.percent>=50" color="cyan"></el-progress>
- <el-progress :percentage="subItem.percent" type="circle" :width="16" :height="16"
- :format="()=>{return ''}" v-else></el-progress>
- </span>
- <span class="media-name"> {{subItem.name}} </span>
- </a>
- </div>
- </li>
- </ul>
- </el-menu>
- <el-menu v-if="required==0">
- <ul class="m-chapter-list" style="width: 400px;font-size: 10px;">
- <li v-for="(item, index) in chapter.normal" :key="index" :class="{'current':item.name==activeChapter}">
- <a href="javascript:void(0)" style="text-decoration: none" @click="goState(item)"
- class="ng-binding">{{parseInt(item.xs/10)}}学时 {{item.name}}</a>
- <div class="sub-list pt10" v-if="item.name==activeChapter">
- <a v-for="(subItem,index) in list" :key="subItem.id" v-if="subItem.chapterName == activeChapter"
- @click="goSubState(subItem, index)" :class="{'current':subItem.name==activeName}">
- <span class="media-process">
- <el-progress :percentage="subItem.percent" type="circle" :width="16" :height="16"
- :format="()=>{return ''}" v-if="subItem.percent>=100" color="green"></el-progress>
- <el-progress :percentage="subItem.percent" type="circle" :width="16" :height="16"
- :format="()=>{return ''}" v-else-if="subItem.percent>=50" color="cyan"></el-progress>
- <el-progress :percentage="subItem.percent" type="circle" :width="16" :height="16"
- :format="()=>{return ''}" v-else></el-progress>
- </span>
- <span class="media-name"> {{subItem.name}} </span>
- </a>
- </a>
- </div>
- </li>
- </ul>
- <!-- <el-menu-item v-for="item in chapter.normal" :key="item">{{item}}</el-menu-item> -->
- </el-menu>
- <div v-if="required==2">
- <i-message :media-id="media.mediaId"></i-message>
- </div>
- </el-col>
- </el-row>
- <div class="left-float" v-if="!closeFace" v-drag v-show="identifyFacePass">
- <video ref="video" width="240" height="180" autoplay></video>
- <canvas ref="canvas" v-show="ontakebtn" width="240" height="180"></canvas>
- </div>
- <el-dialog title="人脸认证" center :visible.sync="identifyFace" width="500px" :close-on-click-modal="false">
- <div style="width: 240px;margin: 100px auto;">
- <video ref="video2" width="240" height="180" autoplay></video>
- <p style="margin-top: 20px;">当前照片:</p>
- <canvas ref="canvas" width="240" height="180"></canvas>
- <p v-if="errMsg" style="font-size: 30px;color: red;"> {{errMsg}}</p>
- </div>
- </el-dialog>
- </div>
- </template>
- <script>
- // import tracking from '@/assets/tracking/build/tracking-min.js';
- // import '@/assets/tracking/build/data/face-min.js';
- import {
- httpServer
- } from "@/components/httpServer/httpServer.js";
- import md5 from 'js-md5';
- import {
- videoPlayer
- } from 'vue-video-player';
- import IMessage from './iMessage.vue'
- import 'video.js/dist/video-js.css'
- // import html2canvas from "html2canvas";
- import {
- MessageBox
- } from "element-ui";
- export default {
- name: "Index",
- data() {
- return {
- timer: false,
- required: 1,
- tickNum: 0,
- prevTime: 0,
- isReady: false,
- ontakebtn: false,
- identifyFace: false,
- identifyFacePass: false,
- activeChapter: '',
- activeName: '',
- curTimes: 0,
- errMsg: '',
- errCount: 0,
- onPlay: false
- }
- },
- components: {
- videoPlayer,
- IMessage
- },
- props: ['media', 'options', 'heartbeat', 'maxErrorCount', 'collectBeat', 'closeFace', 'chapter', 'list'],
- filters: {
- useTime(val) {
- let timestr = ""
- let hour = parseInt(val / 3600);
- let min = parseInt(val / 60 % 60);
- let sec = parseInt(val % 60);
- if (hour < 10) hour = "0" + hour;
- if (min < 10) min = "0" + min;
- if (sec < 10) sec = "0" + sec;
- return hour + ":" + min + ":" + sec
- }
- },
- beforeDestroy() {
- this.stopTick()
- this.reportErr("play", 'destroy');
- },
- directives: {
- drag(el, bindings) {
- el.onmousedown = function(e) {
- var disx = e.pageX - el.offsetLeft
- var disy = e.pageY - el.offsetTop
- document.onmousemove = function(e) {
- el.style.left = e.pageX - disx + 'px'
- el.style.top = e.pageY - disy + 'px'
- }
- document.onmouseup = function() {
- document.onmousemove = document.onmouseup = null
- }
- }
- }
- },
- computed: {
- player() {
- return this.$refs.videoPlayer.player
- }
- },
- beforeDestroy() {
- this.destroyTimer()
- this.closeCamera( "video" )
- },
- created() {
- this.startTick()
- this.startMonitor();
- this.activeChapter = this.media.chapterName;
- this.activeName = this.media.name
- },
- watch: {
- "media.name"() {
- this.activeChapter = this.media.chapterName;
- this.activeName = this.media.name
- this.tickNum = 0
- this.errMsg = ''
- this.errCount = 0
- this.startIdentify()
- this.setposition( this.media.position )
- }
- },
- methods: {
- photograph( ref ) {
- let identify = (ref=="video2");
- let ctx = this.$refs["canvas"].getContext("2d");
- // 把当前视频帧内容渲染到canvas上
- this.ontakebtn = true
- ctx.drawImage(this.$refs[ref], 0, 0, 240, 180);
- // 转base64格式、图片格式转换、图片质量压缩
- let imgBase64 = this.$refs["canvas"].toDataURL("image/jpeg", 1); // 由字节转换为KB 判断大小
- this.ontakebtn = false
- let str = imgBase64.replace("data:image/jpeg;base64,", "");
- let param = {
- id: this.media.id,
- ref,
- image: str
- }
- httpServer("course.collect", param).then(res => {
- let {
- msg,
- pause
- } = res.data
- this.errMsg = msg || '';
- if (msg) {
- this.errCount++
- } else {
- if( identify ){
- this.identifyPassAndPlay()
- }
- this.errCount = 0;
- }
- if (!identify && this.errCount > this.maxErrorCount) {
- this.doPause();
- }
- })
- },
- destroyTimer() {
- if (this.timer) clearInterval(this.timer);
- },
- goState(item) {
- if (item.name == this.activeChapter) {
- this.activeChapter = ""
- } else {
- this.activeChapter = item.name;
- }
- },
- goSubState(item, index) {
- this.$emit('loadMedia', item, index)
- },
- callCamera( ref ) {
- // H5调用电脑摄像头API
- if (this.closeFace) {
- this.identifyFacePass = true;
- return;
- }
- navigator.mediaDevices
- .getUserMedia({
- video: true,
- })
- .then((success) => {
- // 摄像头开启成功
- this.$refs[ref].srcObject = success;
- // 实时拍照效果
- this.$refs[ref].play();
- })
- .catch((error) => {
- this.$message.error(
- "摄像头开启失败,请检查摄像头是否可用!或者打开摄影头"
- );
- console.error("摄像头开启失败,请检查摄像头是否可用!");
- });
- },
- closeCamera( ref ) {
- if (!this.$refs[ref]) return;
- if (!this.$refs[ref].srcObject) return;
- let stream = this.$refs[ref].srcObject;
- let tracks = stream.getTracks();
- tracks.forEach((track) => {
- track.stop();
- });
- this.$refs[ref].srcObject = null;
- },
- startTick() {
- let tick = this.tryTick;
- this.destroyTimer();
- this.tickNum = 0;
- this.timer = setTimeout(tick, 1 * 1000);
- },
- stopTick() {
- if (this.timer) clearInterval(this.timer);
- },
- tryTick() {
- let that = this;
- try {
- that.tick()
- } catch (err) {
- that.reportErr("play", '' + err.message)
- }
- this.destroyTimer()
- this.timer = setTimeout(this.tryTick, 1 * 1000);
- },
- playerReadied(audio) {
- let that = this;
- let {
- position,
- duration
- } = this.media
- if (position > 5 && position < duration) {
- setTimeout(() => {
- this.setposition(position)
- }, 2000)
- }
- this.isReady = true
- },
- onPlayerTimeupdate(player) {
- let curTimes = player.cache_.currentTime;
- if (curTimes > 30 && curTimes > this.curTimes + 2) {
- console.log("return", curTimes, this.media.position)
- player.currentTime(this.curTimes);
- return;
- }
- this.curTimes = curTimes
- },
- setposition(position) {
- if (position > this.media.duration) position = this.media.duration;
- let player = this.$refs.videoPlayer.player;
- let res = player.currentTime(position);
- console.log("setposition", position)
- // player.play()
- this.curTimes = position;
- if (this.media.isFinish) return;
- if (this.media.position >= this.media.duration - 2*this.heartbeat && !this.media.isFinish) {
- this.tick(true)
- }
- },
- onPlayerPause(event) {
- this.reportErr("play", 'pause');
- this.onPlay = false
- },
- onPlayerEnded(event) {
- this.reportErr("play", 'end');
- this.tick(true)
- },
- onClose() {
- this.reportErr("play", 'close')
- this.doPause()
- this.$emit("close")
- this.closeCamera( "video")
- },
- doPause() {
- this.onPlay = false
- let myPlayer = this.$refs.videoPlayer.player;
- myPlayer && myPlayer.pause()
- },
- doPlay() {
- this.onPlay = true
- this.startTick();
- if (!this.$refs.videoPlayer || !this.$refs.videoPlayer.player) return;
- // if (!this.dialog) return this.doPause();
- let myPlayer = this.$refs.videoPlayer.player;
- myPlayer && myPlayer.play()
- this.tickNum = 0
- },
- onPlayerStart( player ) {
- console.log("onPlayerStart")
- this.onPlay = true
- if( !this.identifyFacePass){
- this.startIdentify()
- }
- this.reportErr("play", 'start');
- this.startTick();
- },
- startIdentify(){
- this.identifyFace = true
- this.identifyFacePass = false
- this.closeCamera("video")
- this.callCamera("video2")
- this.startTick()
- },
- identifyPassAndPlay(){
- this.identifyFacePass = true
- this.identifyFace = false;
- this.closeCamera("video2")
- this.callCamera("video")
- this.$message.successMsg("人脸认证通过", 2)
- this.doPlay()
- },
- reportErr(action, msg) {
- httpServer("course.report", {
- action,
- msg
- })
- },
- startMonitor() {
- let that = this
- document.addEventListener("visibilitychange", function() {
- // || document.hidden
- if (document.visibilityState == "hidden") {
- // that.doPause( )
- that.reportErr("play", 'hidden');
- } else {
- that.reportErr("play", 'show');
- // that.doPlay()
- }
- });
- },
- tick(force = false) {
- let media = this.media;
- this.tickNum++
- // 人脸认证期间
- if( !this.identifyFacePass ) {
- if( this.onPlay ){
- this.doPause()
- }
- // 人脸认证
- if (this.tickNum % 3 == 1) {
- this.photograph( "video2" );
- }
- return;
- };
- // 未开始
- if(!force && !this.onPlay ){
- return;
- }
- // 已经完成
- if (this.media.isFinish) {
- console.log("finish")
- return;
- }
- // 每5秒一次心跳
- if (this.tickNum % this.heartbeat != 0) {
- return;
- }
- // if (!this.media.isFinish && this.onPlay && !this.closeFace) {
- // console.log(this.identifyFacePass , this.media.isFinish , this.onPlay, this.closeFace)
- // this.$message.errorMsg("需要安装摄像头才能学习", 2);
- // this.doPause()
- // return;
- // }
- if (this.errCount >= this.maxErrorCount) {
- this.$message.errorMsg("人脸不在摄像头上", 5);
- this.destroyTimer()
- this.$emit("close");
- return;
- }
- let heartBeat = parseInt(this.tickNum / this.heartbeat);
- // 异常 10秒检查
- if (!this.closeFace) {
- if (this.errCount > 0) {
- this.photograph("video")
- } else if (heartBeat % this.collectBeat == 1) {
- this.photograph("video")
- }
- }
- // 主动暂停
- let myPlayer = this.$refs.videoPlayer.player;
- let curTimes = parseInt(myPlayer.currentTime());
- // 后退无心跳
- if( !force ){
- if (curTimes < this.media.position+this.heartbeat) {
- return;
- }
- }
- let isFinish = force ? 1 : 0
- if (curTimes >= media.duration) isFinish = 1;
- // 拉到后面
- if (!isFinish) {
- if (!this.onPlay) return;
- }
- // 强制完成
- let param = {
- id: media.id,
- position: curTimes,
- isFinish
- };
- httpServer("course.tick", param, true).then(res => {
- if (res.code == 200) {
- let {
- skip,
- position,
- pause,
- closed
- } = res.data
- if (pause || closed) {
- this.doPause();
- this.$emit("close")
- if (closed) {
- this.$message.errorMsg("课程关闭学习", 5);
- } else if (pause) {
- this.$message.errorMsg("多处同时播放视频", 5);
- }
- return
- }
- if (!skip) {
- setTimeout(() => {
- this.setposition(position)
- }, 2000);
- };
- Object.assign(param, res.data)
- this.$emit("update", param)
- }
- })
- }
- }
- }
- </script>
- <style>
- @import url("./imedia.css");
- .video-js {
- .vjs-control-bar {
- .vjs-icon-custombutton {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
- }
- .vjs-icon-custombutton:before {
- content: "\f108";
- font-size: 1.8em;
- line-height: 1.67;
- }
- }
- }
- .left-float {
- width: 240px;
- height: 180px;
- background-color: #8bbdf5;
- position: fixed;
- transition: bottom ease .9s;
- z-index: 0;
- left: 60px;
- top: 120px;
- text-align: center;
- border-radius: 5px;
- }
- .p-process {
- width: 100%;
- margin: 20px auto;
- height: 30px;
- }
- .media-footer {
- padding: 0px 30px;
- text-align: left;
- line-height: 40px !important;
- bottom: -10px;
- }
- .media-center {
- text-align: center;
- padding: 0px;
- }
- .media-time {
- font-size: 18px;
- vertical-align: center;
- }
- .media-select {
- white-space: nowrap;
- text-align: right;
- line-height: 40px !important;
- float: right;
- margin: 0px !important;
- }
- .bicon {
- font-size: 28px !important;
- padding: 4px !important;
- }
- .media-el-select {
- font-size: 28px !important;
- width: 80px;
- padding: -4px auto !important;
- }
- .media-name {
- width: 300px;
- margin-left: 16px;
- line-height: 24px;
- white-space: nowrap;
- overflow: hidden;
- align-items: center;
- text-overflow: ellipsis;
- }
- .media-process{
- width: 8px;
- height: 8px;
- align-items: center;
- }
- .vjs-tech {
- pointer-events: none;
- }
- </style>
|