vue 原生方法实现pc端调用摄像头全屏视频考试(实时截屏上传,并提示当前环节

发布时间 2023-05-04 14:08:57作者: SukaLv
<template>
  <div>
    <el-row>
      <el-col :span="10" style="">
        <div>
          <el-card style="margin: 0; padding:0; overflow-y: auto">
            <div style="width:100%; min-height:600px;position: relative;">
              <!--(防止页面方法缩小弹幕位置偏差) 思路:每次页面浏览器狂发生变化时获得video的offsetWidth, 每次发生变化时重新设置弹幕的margin-left/left -->
              <barrage ref="barrage" :options="paperData.randList" :videoDisabled="videoDisabled" class="barrage"></barrage>
              <video id="live" style="min-height:600px!important;box-sizing: border-box;" > </video>
            </div>
          </el-card>
          <el-card style=" overflow-y: auto">
             <el-row style="text-align: center;"  :gutter="10">
              <el-col :span="24" v-if="!videoDisabled">
                剩余时间: <exam-timer v-model="paperData.leftSecondsVideo" type="video"  @timeout="doHandler(1)" />
              </el-col>
              <el-col :span="24" v-else>
                剩余考试时间: 
                <span style="color: #ff0000; font-weight: 700">{{ min }}分钟{{ sec }}秒</span>
                <!-- <exam-timer v-model="paperData.leftSecondsVideo" type="video" timerTime="slow" :time-map="paperData.timeMap"  @timeout="doHandler(1)" /> -->
              </el-col>
              <el-col :span="24">
                <el-divider />
              </el-col>
              <el-col :span="24">
                <button id="start" :disabled="videoDisabled" :class="!videoDisabled?'el-button el-button--primary el-button--small':'el-button el-button--primary el-button--medium'">{{!videoDisabled?'点击开始录制':'正在录制'}}</button>
                <button id="stop"  class="el-button el-button--success el-button--small">点击保存并上传</button>
              </el-col>
              <el-col :span="24">
                <el-divider />
              </el-col>
            </el-row>
          </el-card>
        </div>
    <!-- <button @click="updata">点击保存并上传上传</button> -->
      </el-col>
      <el-col :span="14">
        <el-card style="height: 99vh; overflow-y: auto">
          <el-form ref="practiceForm" :model="practiceForm" :rules="practiceRules">
            <el-table
              :data="paperData.dtlList"
              :span-method="objectSpanMethod"
              border
              style="width: 100%; margin-top: 20px">
              <el-table-column type="index" label="序号" align="center"/>
              <el-table-column
                prop="category"
                label="提示1"
                min-width="50" align="center">
              </el-table-column>
              <el-table-column
                prop="stepNum"
                label="提示2" min-width="70" align="center">
                <template #default="{row}">
                  {{handleStepSelect(row)}}
                </template>
              </el-table-column>
              <el-table-column
                prop="speech"
                label="参考话术3" min-width="100" align="center" >
                <template #default="{row}">
                  {{row.speech}}
                </template>
              </el-table-column>
              <el-table-column
                prop="archives"
                label="文字信息" min-width="40" align="center">
                  <template #default="{row}">
                    <div v-html="handleArchives(row.archives)"></div>
                  </template>
              </el-table-column>
              <el-table-column
                prop="remark"
                label="备注" align="center">
                <template #default="{row}">
                  {{row.remark}}
                </template>
              </el-table-column>
            </el-table>
          </el-form>
        </el-card>
      </el-col>
    </el-row>
  <div v-show="progressShow" style="width:100%;height:100%; background:rgb(255,255,255,.8);position:fixed;top:0;left:0">
      <el-progress type="circle" :percentage="progress>100 ? 100:progress" ></el-progress>                                 
  </div>
  </div>
</template>

<script>
import ExamTimer from './components/ExamTimer' // 组件
import screenfull from 'screenfull' // 组件
import Barrage from './components/Barrage'
import {paperDetail,getarchivesbykey, uploadVideo,captureImage, upsmileScore, uponetime,videouploadAili } from '@/api/web/practiceExam'
export default {
    name:'ExamVideo',
    components:{
      ExamTimer,
      screenfull,
      Barrage
    },
  data() {
    return {
      mediaRecorderData: "",
      n:0,
      blobData:"",
      paperId:null,
      paperData:{},
      practiceForm:{ dtlList:[]},
      practiceRules:{ },
      spanArr:[],
      position:null,
      buzhouOptions: [],
      video:null,
      canvas: null,
      canvasContext:null,
      timer:'',
      numTimer: null,
      num:0,
      videoDisabled: false,
      handleStop: null,
      streamorigin: null,
      progress:0, //进度
      progressShow:false,
      min: '00',
      sec:'00',
      leftTimer: null,
      timemap: null,
      timeMap: null,
    };
  },
  destroyed(){
    clearInterval(this.leftTimer)
    clearInterval(this.timer)
    clearInterval(this.numTimer)
      
  },
  mounted() {
    // 防止页面后退
    history.pushState(null, null, document.URL)
    window.addEventListener('popstate', function() {
      history.pushState(null, null, document.URL)
    })
    // 防止页面刷新
    window.onbeforeunload = function(event){
      return false
    },
    document.addEventListener('fullscreenchange', v=>{
      if(!screenfull.isFullscreen){
        this.doHandler(1)
      }
    })
    this.stopF5Refresh()
    this.getNavigator()
    this.video = document.querySelectorAll('#video')
    this.canvas = document.createElement('canvas')
    this.canvas.width = 400
    this.canvas.height = 300
    this.canvasContext = this.canvas.getContext('2d')
  },
  async created() {
    const id = this.$route.params.id
    if (typeof id !== 'undefined') {
      this.paperId = id
      await this.fetchData(id)
      this.handleConfirm()
      this.getDicts("sys_dict_video_buzhou").then(response => {
        this.buzhouOptions = response.data;
      });
    }
     this.video = document.querySelectorAll('#video')
    this.canvas = document.createElement('canvas')
      this.canvas.width = 400
      this.canvas.height = 300
      this.canvasContext = this.canvas.getContext('2d')
  },
  methods: {
    stopF5Refresh() {
      document.onkeydown = function(e) {
          var evt = window.event || e;
          var code = evt.keyCode || evt.which;
          //屏蔽F1---F12
          if ((code > 111 && code < 124) || (code == 17 || code == 18 || code==82)) {
              if (evt.preventDefault) {
                  evt.preventDefault();
              } else {
                  evt.keyCode = 0;
                  evt.returnValue = false;
              }
          }
      };
      //禁止鼠标右键菜单
      document.oncontextmenu = function(e) {
          return false;
      };
  },
    handleTimeMap(){
      let newValue = this.timeMap
      const keys = Object.keys(newValue)
      keys.sort(function(a, b){
        return newValue[b] - newValue[a]
      })
      this.timemap = keys
    },
    handleConfirm(){
      this.$confirm('开场互动环节', '直播考試环节', {
          confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning',
        }).then(async (valid)=>{
            await uponetime({id: this.paperData.id})
            const res = await paperDetail({ id: this.paperId })
            this.paperData.leftSecondsVideo = res.data.leftSecondsVideo
            // this.paperData.leftSecondsVideo = 20
            this.leftTimer = setInterval(() =>this.countdown(), 1000)
            this.toggleFull()
            if(document.all) {
                document.getElementById("start").click();
            }else { 
              var e = document.createEvent("MouseEvents");e.initEvent("click", true, true);
              document.getElementById("start").dispatchEvent(e);
            }
          }).catch(() => {
            this.handleConfirm()
          })
    },
    countdown(){
      if (this.paperData.leftSecondsVideo <= 0) {
        this.doHandler(1)
        return
      }
      

      const min = parseInt(this.paperData.leftSecondsVideo / 60)
      const sec = parseInt(this.paperData.leftSecondsVideo % 60)

      this.min = min > 9 ? min : '0' + min
      this.sec = sec > 9 ? sec : '0' + sec
      this.paperData.leftSecondsVideo -= 1

      
      if(this.paperData.leftSecondsVideo == parseInt(this.timeMap[this.timemap[3]])){
        this.handlerAlert(this.timemap[3])
      }else if(this.paperData.leftSecondsVideo == parseInt(this.timeMap[this.timemap[2]])){
        this.handlerAlert(this.timemap[2])
      }else if(this.paperData.leftSecondsVideo == parseInt(this.timeMap[this.timemap[1]])){
        this.handlerAlert(this.timemap[1])
      }
    },
    handlerAlert(msg){
      this.$message({
        message: '当前环节进入'+ msg,
        type: 'success',
        showClose: true,
        duration:10000
      });
    },
    toggleFull() {
      if (!screenfull.isFullscreen) {
        screenfull.toggle()
        this.$message({
          message: ' 本场考试已开启切屏监控,请保持全屏,离开将会强制交卷,请诚信考试!',
          type: 'error',
          showClose: true,
          duration:20000
        });
      }
    },
    fetchData(){
       paperDetail({ id: this.paperId }).then(response => {
        // 试卷内容
        if(response.code!==200){
          this.$message.error(res.msg)
          return false;
        }
          this.paperData = Object.assign({},response.data)
          this.timeMap = response.data.timeMap
          this.handleTimeMap()
          // this.paperData.leftSecondsVideo = 10
          response.data.dtlList.forEach(list=>{
            getarchivesbykey({key:list.step}).then((res)=>{
              if(res.code == 200){
              list.archives = res.msg
            }
            })
          })
          if(this.paperData.dtlList.length>0){
            this.paperData.dtlList = response.data.dtlList.sort(this.compare('stepNum'))
          }
          this.getSpanArr(this.paperData.dtlList)
      })
    },
    getNavigator(){
      
      let stopButton = document.getElementById("stop");
      let startButton = document.getElementById("start");

      navigator.mediaDevices
      .getUserMedia({
        audio: true,
        video: true,
      })
      .then((stream) => {
        // console.log(stream, "stream");
        let liveVideo = document.getElementById("live");
        // liveVideo.src = URL.createObjectURL(stream); // 你会看到一些警告
        liveVideo.srcObject = stream;
        this.streamorigin = stream
        liveVideo.play();

        stopButton.addEventListener("click", this.stopLive);
        startButton.addEventListener("click", (e) => {
          this.startLive(stream);
        });
      });
    },
    // 暂停后下载视频
    downLoadVideo(chunks) {
        let downloadLink = document.createElement("a");
        downloadLink.href = URL.createObjectURL(
            new Blob(chunks, {
                type: "application/video",
            })
        );
        // downloadLink.download = 'live.webm';
        // downloadLink.download = "live.ogg";
        downloadLink.download = "live.mp4";
        downloadLink.click();
    },

    // 结束录制
    stopLive() {
      this.n = 0;
      if (this.mediaRecorderData&&this.mediaRecorderData!=null&&this.mediaRecorderData!='') { 
        this.mediaRecorderData.stop();
      } else {
        this.$message.error("还没有开始。");
      }
    },

    // 开始
    startLive(stream) {
        let recordedChunks = [];
        this.mediaRecorderData = new MediaRecorder(stream);

        this.mediaRecorderData.start();

        this.mediaRecorderData.addEventListener("dataavailable", (e)=> {
            if (e.data.size > 0) {
              recordedChunks.push(e.data);
              this.blobData = recordedChunks;
            };
        });
        
        this.mediaRecorderData.addEventListener("stop", async ()=>{
          // console.log("暂停 自动下载");
          // this.downLoadVideo(recordedChunks);
          await this.updata()
          await upsmileScore({id: this.paperId})
          clearInterval(this.timer) 
          clearInterval(this.numTimer)
          clearInterval(this.leftTimer)
          this.num = 0
        });

        this.mediaRecorderData.addEventListener("start", (e) => {
          // console.log("开始 录制");
          this.videoDisabled = true
          this.timer = setInterval(() =>this.captureImage(), 30000)
          this.numTimer = setInterval(() =>this.handleTiming(), 1000)
        });
    },

    compare(property){
      return (a,b)=>{
        const value1 = a[property]
        const value2 = b[property]
        return value1-value2
      }
    },
    // 上传视频
    async doHandler(num){
      clearInterval(this.timer) 
      clearInterval(this.numTimer)
      clearInterval(this.leftTimer)
      if(!this.videoDisabled){
        this.updata()
      }else{
        let stopButton = document.getElementById("stop");
        if(stopButton)  stopButton.click()
      }
    },
    updata(num){
      if(this.blobData){
        var file = new File(this.blobData, 'video-' + (new Date).toISOString().replace(/:|\./g, '-') + '.mp4', {
          type: 'video/mp4'
        })
      }else{
        var file = ''
      }
      const data = new FormData();
      data.append('file', file);
      data.append('id', this.paperId);
      data.append('videoTime', this.num)
      var onUploadProgress= progressEvent => {
            this.$message.closeAll()
            this.progressShow = true
            var complete = (progressEvent.loaded / progressEvent.total * 100 | 0)
            this.progress = complete
        }
      uploadVideo(data, onUploadProgress).then(response=>{
        if(response.code!==200){
          this.$message.error(response.msg)
        }else {
          this.num = 0
          if (screenfull.isFullscreen) {
            screenfull.toggle()
          }
          this.progressShow = false
          this.$router.push({ name: 'ShowExam', params: { id: this.paperId, type:2,mode:'0' }})
        }
      })
    },
    getSpanArr(data){
      data.forEach((item, i) => {
        if(i===0){
          this.spanArr.push(1)
          this.position = 0
        }else{
          if(data[i].category == data[i-1].category){
            this.spanArr[this.position] += 1
            this.spanArr.push(0);
          }else{
            this.spanArr.push(1)
            this.position = i
          }
        }
      })
    },
    objectSpanMethod({ row, column, rowIndex, columnIndex }) {
      if (columnIndex === 1 ||(columnIndex==4 &&(rowIndex==8||rowIndex==9||rowIndex==10||rowIndex==11||rowIndex==12))) {
        const _row = this.spanArr[rowIndex]
        const _col = this.spanArr[columnIndex]
          return {
            rowspan: _row,
            colspan: 1
          };
      }
    },
    handleArchives(s){
      let html = ''
      if(s){
        const flag = s.indexOf(",") !=-1
        const sName = s.split(",")
        let kk = ''
        for(let index = 0; index < sName.length; index++) {
         kk+=` <div>${sName[index]}</div>`
        }
        html = flag ? kk : s
      }
      return html
    },
    handleStepSelect(row){
      const obj = this.buzhouOptions.find(option=>row.step == option.dictValue)
      return obj && obj.dictLabel ? obj.dictLabel:''
    },
    handleTiming(){
      this.num+=1
      //  console.log(this.num)
    }, 
    captureImage() {//上传截图
      const video = document.getElementById('live')

      const canvas = document.createElement('canvas')    //创建一个canvas
      if(video&& canvas){
        canvas.width = video.offsetWidth
        canvas.height = video.offsetHeight

        canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height)//绘制图像
        const img = new Image()                                                    //创建img
        img.src = canvas.toDataURL('image/png')
        const blobFile = this.dataURLtoFile(img.src, Date.parse(new Date())+'.jpg')
        const data = new FormData();
        data.append('file', blobFile);
        data.append('id', this.paperId);
        captureImage(data).then(response=>{
          if(response.code!==200){
            this.$message.error(response.msg)
          }
        })
      }
      
    },
    //将base64转换为blob
    dataURLtoFile(dataurl, filename) {
      //将base64转换为文件
      var arr = dataurl.split(","),
        mime = arr[0].match(/:(.*?);/)[1],
        bstr = atob(arr[1]),
        n = bstr.length,
        u8arr = new Uint8Array(n);
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return new File([u8arr], filename, {
        type: mime,
      });
    }
  },
};
</script>

<style scoped>
#live{
  position: absolute;
  top: 50%;   
  left: 50%;   
  background-color: #000;
  -webkit-transform: translateX(-50%) translateY(-50%); 
  transform: translateX(-50%) translateY(-50%); 
}
::v-deep .el-divider--horizontal{
  margin: 1vh 0;
}
::v-deep .el-card.is-always-shadow {
    box-shadow:none;
  }
.barrage{
  position: absolute;
  width: 500px;
  bottom: 6%;   
  left: 50%;  
  margin-left:-380px;
  z-index: 99999999999999999;
}
::v-deep .el-progress.el-progress--circle{
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translateX(-50%) translateY(-50%);
}
</style>