y595705120 1 gadu atpakaļ
vecāks
revīzija
c0fa91019a

+ 271 - 0
src/components/face/icollect.vue

@@ -0,0 +1,271 @@
+<template>
+  <div class="see">
+    <video
+      ref="myVideo"
+      muted
+      loop
+      playsinline
+      @loadedmetadata="fnRun"
+    ></video>
+    <canvas ref="myCanvas" />
+  </div>
+</template>
+
+<script>
+import * as faceapi from "face-api.js";
+export default {
+  name: "WebRTCFaceRecognition",
+  data() {
+    return {
+      nets: "ssdMobilenetv1", // 模型
+      options: null, // 模型参数
+      withBoxes: true, // 框or轮廓
+      detectFace: "detectSingleFace", // 单or多人脸
+      detection: "landmark",
+      videoEl: null,
+      canvasEl: null,
+      timeout: 0,
+      // 视频媒体参数配置
+      constraints: {
+        audio: false,
+        video: {
+          // ideal(应用最理想的)
+          width: {
+            min: 320,
+            ideal: 320,
+            max: 320,
+          },
+          height: {
+            min: 240,
+            ideal: 240,
+            max: 240,
+          },
+          // frameRate受限带宽传输时,低帧率可能更适宜
+          frameRate: {
+            min: 15,
+            ideal: 30,
+            max: 60,
+          },
+          // 显示模式前置后置
+          facingMode: "environment",
+        },
+      },
+    };
+  },
+  // watch: {
+  //   detection(val) {
+  //     this.detection = val;
+  //     this.videoEl.pause();
+  //     setTimeout(() => {
+  //       this.videoEl.play();
+  //       setTimeout(() => this.fnRun(), 300);
+  //     }, 300);
+  //   },
+  // },
+  mounted() {
+    console.log("mounted")
+    this.$nextTick(() => {
+      this.fnInit();
+    });
+  },
+  methods: {
+    // 初始化模型加载
+    async fnInit() {
+      console.log("fnInit")
+       await faceapi.nets[this.nets].loadFromUri("/static/models"); // 算法模型
+      console.log("loadFromUri")
+       await faceapi.loadFaceLandmarkModel("/static/models"); // 轮廓模型
+      console.log("loadFaceLandmarkModel")
+      // await faceapi.loadFaceExpressionModel("/models"); // 表情模型
+      // await faceapi.loadAgeGenderModel("/models"); // 年龄模型
+      // 根据算法模型参数识别调整结果
+      this.options = new faceapi.SsdMobilenetv1Options({
+        minConfidence: 0.5, // 0.1 ~ 0.9
+      });
+      console.log("options", this.options)
+      // 节点属性化
+      this.videoEl = this.$refs["myVideo"]
+      // document.getElementById("myVideo");
+      this.canvasEl = this.$refs["myCanvas"]
+      // document.getElementById("myCanvas");
+      setTimeout(() => this.fnOpen(), 1000);
+      console.log("init", this.canvasEl, this.videoEl )
+    },
+    // 人脸面部勘探轮廓识别绘制
+    async fnRunFaceLandmark() {
+      console.log("RunFaceLandmark");
+      console.log("paused", this.videoEl.paused)
+      if (this.videoEl.paused) return clearTimeout(this.timeout);
+      console.log("paused", this.videoEl.paused)
+      // 识别绘制人脸信息
+      const result = await faceapi[this.detectFace](
+        this.videoEl,
+        this.options
+      ).withFaceLandmarks();
+
+      console.log("result", result)
+      if (result && !this.videoEl.paused) {
+        const dims = faceapi.matchDimensions(this.canvasEl, this.videoEl, true);
+        const resizeResult = faceapi.resizeResults(result, dims);
+        this.withBoxes
+          ? faceapi.draw.drawDetections(this.canvasEl, resizeResult)
+          : faceapi.draw.drawFaceLandmarks(this.canvasEl, resizeResult);
+      } else {
+        this.canvasEl
+          .getContext("2d")
+          .clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
+      }
+      this.timeout = setTimeout(() => this.fnRunFaceLandmark());
+    },
+    // 人脸表情识别绘制
+    async fnRunFaceExpression() {
+      if (this.videoEl.paused) return clearTimeout(this.timeout);
+      // 识别绘制人脸信息
+      const result = await faceapi[this.detectFace](this.videoEl, this.options)
+        .withFaceLandmarks()
+        .withFaceExpressions();
+      if (result && !this.videoEl.paused) {
+        const dims = faceapi.matchDimensions(this.canvasEl, this.videoEl, true);
+        const resizeResult = faceapi.resizeResults(result, dims);
+        this.withBoxes
+          ? faceapi.draw.drawDetections(this.canvasEl, resizeResult)
+          : faceapi.draw.drawFaceLandmarks(this.canvasEl, resizeResult);
+        faceapi.draw.drawFaceExpressions(this.canvasEl, resizeResult, 0.05);
+      } else {
+        this.canvasEl
+          .getContext("2d")
+          .clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
+      }
+      this.timeout = setTimeout(() => this.fnRunFaceExpression());
+    },
+    // 年龄性别识别绘制
+    async fnRunFaceAgeAndGender() {
+      if (this.videoEl.paused) return clearTimeout(this.timeout);
+      // 识别绘制人脸信息
+      const result = await faceapi[this.detectFace](this.videoEl, this.options)
+        .withFaceLandmarks()
+        .withAgeAndGender();
+      if (result && !this.videoEl.paused) {
+        const dims = faceapi.matchDimensions(this.canvasEl, this.videoEl, true);
+        const resizeResults = faceapi.resizeResults(result, dims);
+        this.withBoxes
+          ? faceapi.draw.drawDetections(this.canvasEl, resizeResults)
+          : faceapi.draw.drawFaceLandmarks(this.canvasEl, resizeResults);
+        if (Array.isArray(resizeResults)) {
+          resizeResults.forEach((result) => {
+            const { age, gender, genderProbability } = result;
+            new faceapi.draw.DrawTextField(
+              [
+                `${Math.round(age, 0)} years`,
+                `${gender} (${Math.round(genderProbability)})`,
+              ],
+              result.detection.box.bottomLeft
+            ).draw(this.canvasEl);
+          });
+        } else {
+          const { age, gender, genderProbability } = resizeResults;
+          new faceapi.draw.DrawTextField(
+            [
+              `${Math.round(age, 0)} years`,
+              `${gender} (${Math.round(genderProbability)})`,
+            ],
+            resizeResults.detection.box.bottomLeft
+          ).draw(this.canvasEl);
+        }
+      } else {
+        this.canvasEl
+          .getContext("2d")
+          .clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
+      }
+      this.timeout = setTimeout(() => this.fnRunFaceAgeAndGender());
+    },
+    // 执行检测识别类型
+    fnRun() {
+      if (this.detection === "landmark") {
+        this.fnRunFaceLandmark();
+        return;
+      }
+      if (this.detection === "expression") {
+        this.fnRunFaceExpression();
+        return;
+      }
+      if (this.detection === "age_gender") {
+        this.fnRunFaceAgeAndGender();
+        return;
+      }
+    },
+    // 启动摄像头视频媒体
+    fnOpen() {
+      if (typeof window.stream === "object") return;
+      clearTimeout(this.timeout);
+      this.timeout = setTimeout(() => {
+        clearTimeout(this.timeout);
+        navigator.mediaDevices
+          .getUserMedia({
+            video: true,
+          })
+          .then(this.fnSuccess)
+          .catch(this.fnError);
+      }, 300);
+    },
+    // 成功启动视频媒体流
+    fnSuccess(stream) {
+      console.log("stream", stream )
+      window.stream = stream; // 使流对浏览器控制台可用
+      this.videoEl.srcObject = stream;
+      console.log("this.videoEl", this.videoEl )
+      this.videoEl.play();
+    },
+    // 失败启动视频媒体流
+    fnError(error) {
+      console.log(error);
+      alert("视频媒体流获取错误" + error);
+    },
+    // 结束摄像头视频媒体
+    fnClose() {
+      this.canvasEl
+        .getContext("2d")
+        .clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
+      this.videoEl.pause();
+      clearTimeout(this.timeout);
+      if (typeof window.stream === "object") {
+        window.stream.getTracks().forEach((track) => track.stop());
+        window.stream = "";
+        this.videoEl.srcObject = null;
+      }
+    },
+  },
+  beforeDestroy() {
+    this.fnClose();
+  },
+};
+</script>
+
+<style scoped>
+button {
+  height: 30px;
+  border: 2px #42b983 solid;
+  border-radius: 4px;
+  background: #42b983;
+  color: white;
+  margin: 10px;
+}
+.see {
+  position: relative;
+}
+.see canvas {
+  position: absolute;
+  top: 0;
+  left: 0;
+}
+.option {
+  padding-bottom: 20px;
+}
+.option div {
+  padding: 10px;
+  border-bottom: 2px #42b983 solid;
+}
+.option div label {
+  margin-right: 20px;
+}
+</style>

+ 8 - 0
src/containers/center/face/index.vue

@@ -0,0 +1,8 @@
+<template>
+</template>
+
+<script>
+</script>
+
+<style>
+</style>

+ 14 - 10
src/containers/center/play/components/iCourseInfo.vue

@@ -44,19 +44,23 @@
         <div>
           <p style="font-size: 24px;"> 说明 </p>
           <p class="mt10" >岗位名称:{{tpl.name}}<strong style="color: red;">{{tpl.nd}}</strong></p>
+
           <p class="mt10">
-            学习截至时间:
-            <strong style="color: red;">
-            {{info.startDate|add80Date}}
-            </strong>
+              课程学时: <strong style="color: red;">{{tpl.xs/10}}</strong>
+            <span class="ml20">
+              视频学时: <strong style="color: red;">{{tpl.mediaXs/10}}</strong>
+            </span>
+          </p>
+          <p class="mt10">
+              复习学时: <strong style="color: red;">{{tpl.testXs/10}}</strong>
+            <span class="ml20">
+              考试学时: <strong style="color: red;">{{tpl.examXs/10}}</strong>
+            </span>
           </p>
-          <p class="mt10" style="color: red;" v-if="tpl.examGroupId>0" >报名成功,80天内完成学习与考试</p>
-          <p class="mt10" v-if="tpl.examGroupId==0" >学完所有课程,即可打印学时证明</p>
-
           <div>
-              <!-- <el-button type="primary" class="mt10" style="font-size: 14px;" @click="startExam" v-if="tpl.examGroupId>0" :disabled="info.score>=60">
+              <el-button type="primary" class="mt10" style="font-size: 14px;" @click="startExam" v-if="tpl.examGroupId>0" :disabled="info.score>=60">
                 参加考试
-              </el-button> -->
+              </el-button>
               <el-button type="primary" class="mt10" style="font-size: 14px;" @click="printCert" v-if="tpl.tplId>0">
                 学时证明
               </el-button>
@@ -97,7 +101,7 @@
          formatFinish() {
         let {gxs,axs} = this.info;
         if( !axs ) axs = 1;
-        return `获得${gxs}学时, 总共${axs}学时`;
+        return `获得${gxs}学时, 视频${axs}学时`;
       },
       formatExam() {
         let {score} = this.info

+ 471 - 0
src/containers/center/play/components/iMedia.vue.face

@@ -0,0 +1,471 @@
+<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" :playsinline="true"
+          @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 style="width: 8px;height: 8px;">
+                    <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 style="margin-left: 16px;"> {{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 style="width: 8px;height: 8px;">
+                    <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 style="margin-left: 16px;"> {{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>
+     <i-collect></i-collect>
+    </div>
+
+  </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 ICollect from "@/components/face/icollect.vue";
+  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,
+        isnotbtn: false,
+        ontakebtn: false,
+        activeChapter: '',
+        activeName: '',
+        curTimes: 0,
+        errMsg: '',
+        errCount: 0,
+        onPlay: false
+      }
+    },
+    components: {
+      videoPlayer,
+      IMessage,
+      ICollect
+    },
+    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()
+    },
+    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.setposition( this.media.position )
+      }
+    },
+    methods: {
+      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)
+      },
+      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.stopTick()
+        this.onPlay = false
+      },
+      onPlayerEnded(event) {
+        this.reportErr("play", 'end');
+        this.tick(true)
+      },
+      onClose() {
+        this.reportErr("play", 'close')
+        this.doPause()
+        this.$emit("close")
+        this.closeCamera()
+      },
+      doPause() {
+        this.stopTick()
+        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() {
+        console.log("onPlayerStart")
+        this.reportErr("play", 'start');
+        this.startTick();
+        this.onPlay = true
+        this.tickNum = 0
+      },
+      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.media.isFinish) {
+          console.log("finish")
+          return;
+        }
+        // 每5秒一次心跳
+        if (this.tickNum % this.heartbeat != 0) {
+          return;
+        }
+        if (!this.isnotbtn && !this.media.isFinish && this.onPlay && !this.closeFace) {
+          console.log(this.isnotbtn , 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 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;
+  }
+</style>

+ 0 - 13
src/containers/center/play/index.vue

@@ -221,24 +221,11 @@
         this.info = Object.assign(this.info, param);
       },
       startExamTest(){
-        if( !this.isStudyFinish() ){
-          this.$message.errorMsg(" 完成学习才能专项练习", 2)
-          return;
-        }
         this.testExamDalog = true;
       },
-      isStudyFinish(){
-        let {getXs,totalXs} = this.info
-        let {testXs, examXs} = this.tpl;
-        return getXs>=totalXs -testXs - examXs
-      },
       startExam( groupId ){
         let courseId = this.courseId
         let endDate = new Date( )
-        if( !this.isStudyFinish() ){
-          this.$message.errorMsg(" 完成学习才能考试", 2)
-          return;
-        }
         delExam()
         this.$router.push({path:`/center/exam/${courseId}`, query:{groupId}});
       },

+ 128 - 0
src/containers/center/play/mediaPlay.vue

@@ -0,0 +1,128 @@
+<template>
+  <div class="m-right-block fr mh576" style="position: relative;">
+    <div style="width: 640px;margin: 20px auto;">
+    <video-player class="video-player"  
+      ref="videoPlayer"   
+      :playsinline="true"
+      :options="playerOptions"   
+      @play="onPlayerPlay($event)"
+      @pause="onPlayerPause($event)"
+      @ended="onPlayerEnded($event)"
+      @waiting="onPlayerWaiting($event)"
+      @playing="onPlayerPlaying($event)"
+      @loadeddata="onPlayerLoadeddata($event)"
+      @timeupdate="onPlayerTimeupdate($event)"
+      @canplay="onPlayerCanplay($event)"
+      @canplaythrough="onPlayerCanplaythrough($event)"
+      @statechanged="playerStateChanged($event)"
+      @ready="playerReadied" />
+        </div>
+    </div>
+  </template>
+    <script>
+      import {
+        videoPlayer
+      } from 'vue-video-player';
+      import 'video.js/dist/video-js.css'
+      export default {
+        name: "VideoWatch",
+        components:{videoPlayer},
+        data() {
+          return {
+            playerOptions: {
+              controls: true, // 是否显示控制栏
+              // playbackRates: [0.5, 1.0, 1.5, 2.0], //播放速度
+              autoplay: true, //如果true,浏览器准备好时开始回放。
+              muted: false, // 默认情况下将会消除任何音频。
+              loop: false, // 导致视频一结束就重新开始。
+              preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
+              language: 'zh-CN',
+              aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
+              fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
+              sources: [{
+                src:"https://media.ndjsxh.cn:18443/hls/2467/2467.m3u8",
+                type: "application/x-mpegURL"
+              }],
+              poster: "", //你的封面地址
+              width: document.documentElement.clientWidth,
+              notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖Video.js无法播放媒体源时显示的默认信息。
+              controlBar: {
+                timeDivider: true, //当前时间和持续时间的分隔符
+                durationDisplay: true, //显示持续时间
+                remainingTimeDisplay: true, //是否显示剩余时间功能
+                fullscreenToggle: false //全屏按钮
+              }
+            },
+            playtimes: "10", //视频观看起点
+          };
+        },
+        created() {
+          let that = this
+          setTimeout(()=>{
+            that.playtimes = "100"
+          }, 3000)
+          // this.fetchData(); //获取的视频从何处播放的时间点
+          // this.playerOptions.sources[0].src = '视频地址' //视频地址
+        },
+        mounted() {},
+        computed: {
+          player() {
+            return this.$refs.videoPlayer.player
+          }
+        },
+        methods: {
+          // 播放回调
+          onPlayerPlay(player) {
+            console.log('player play!', player)
+          },
+          // 暂停回调
+          onPlayerPause(player) {
+            console.log('player pause!', player)
+          },
+          // 视频播完回调
+          onPlayerEnded($event) {
+            console.log("onPlayerEnded", $event)
+          },
+          // DOM元素上的readyState更改导致播放停止
+          onPlayerWaiting($event) {
+            console.log("onPlayerWaiting", $event)
+          },
+          // 已开始播放回调
+          onPlayerPlaying($event) {
+            console.log("onPlayerPlaying", $event)
+          },
+          // 当播放器在当前播放位置下载数据时触发
+          onPlayerLoadeddata($event) {
+            console.log("onPlayerLoadeddata", $event)
+          },
+          // 当前播放位置发生变化时触发。
+          onPlayerTimeupdate(player) {
+            this.gklog = player.cache_.currentTime
+            // console.log(' onPlayerTimeupdate!', this.gklog)
+          },
+          //媒体的readyState为HAVE_FUTURE_DATA或更高
+          onPlayerCanplay(player) {
+            console.log('player Canplay!', player)
+          },
+          //媒体的readyState为HAVE_ENOUGH_DATA或更高。这意味着可以在不缓冲的情况下播放整个媒体文件。
+          onPlayerCanplaythrough(player) {
+            console.log('player Canplaythrough!', player)
+          },
+          //播放状态改变回调
+          playerStateChanged(playerCurrentState) {
+            // console.log('player current update state', playerCurrentState)
+          },
+          //将侦听器绑定到组件的就绪状态。与事件监听器的不同之处在于,如果ready事件已经发生,它将立即触发该函数。。
+          playerReadied(player, playtimes) {
+            player.currentTime(playtimes)
+            console.log('example player 1 readied', player);
+          },
+        },
+        watch: {
+          playtimes(val, oldVal) { //普通的watch监听
+            console.log("playtimes: " + val);
+            this.playerReadied(this.$refs.videoPlayer.player, val);
+          },
+        }
+      };
+    </script>

BIN
static/models/age_gender_model-shard1


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/models/age_gender_model-weights_manifest.json


BIN
static/models/face_expression_model-shard1


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/models/face_expression_model-weights_manifest.json


BIN
static/models/face_landmark_68_model-shard1


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/models/face_landmark_68_model-weights_manifest.json


BIN
static/models/face_landmark_68_tiny_model-shard1


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/models/face_landmark_68_tiny_model-weights_manifest.json


BIN
static/models/face_recognition_model-shard1


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/models/face_recognition_model-shard2


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/models/face_recognition_model-weights_manifest.json


BIN
static/models/mtcnn_model-shard1


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/models/mtcnn_model-weights_manifest.json


BIN
static/models/ssd_mobilenetv1_model-shard1


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/models/ssd_mobilenetv1_model-shard2


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/models/ssd_mobilenetv1_model-weights_manifest.json


BIN
static/models/tiny_face_detector_model-shard1


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/models/tiny_face_detector_model-weights_manifest.json


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels