zhangjie 4 жил өмнө
parent
commit
755e7978f8

+ 4 - 0
src/api/invigilation.js

@@ -199,6 +199,10 @@ export function sendWarningMsg(datas) {
   const data = pickBy(datas, (v) => v !== "");
   const data = pickBy(datas, (v) => v !== "");
   return httpApp.post("/api/admin/invigilate/notice", data);
   return httpApp.post("/api/admin/invigilate/notice", data);
 }
 }
+// TODO:接口待定
+export function sendAudioWarningMsg(datas) {
+  return httpApp.post("/api/admin/invigilate/notice", datas);
+}
 
 
 // reexam-apply
 // reexam-apply
 export function reexamApplyList(datas) {
 export function reexamApplyList(datas) {

BIN
src/assets/icon-play-act.png


BIN
src/assets/icon-play.png


+ 19 - 4
src/features/invigilation/RealtimeMonitoring/WarningDetail.vue

@@ -124,12 +124,12 @@
             @click="toSendTextMsg"
             @click="toSendTextMsg"
             title="发送文字提醒"
             title="发送文字提醒"
           ></el-button>
           ></el-button>
-          <!-- todo: -->
-          <!-- <el-button
+          <el-button
             class="el-icon-btn"
             class="el-icon-btn"
             type="primary"
             type="primary"
             icon="icon icon-audio"
             icon="icon icon-audio"
-          ></el-button> -->
+            @click="toSendAudioMsg"
+          ></el-button>
           <el-popover
           <el-popover
             class="warning-body-head-call"
             class="warning-body-head-call"
             placement="bottom-start"
             placement="bottom-start"
@@ -216,6 +216,7 @@
       </div>
       </div>
     </div>
     </div>
 
 
+    <!-- student-breach-dialog -->
     <student-breach-dialog
     <student-breach-dialog
       :instance="curDetail"
       :instance="curDetail"
       ref="StudentBreachDialog"
       ref="StudentBreachDialog"
@@ -225,6 +226,11 @@
       :record-id="recordId"
       :record-id="recordId"
       ref="WarningTextMessageDialog"
       ref="WarningTextMessageDialog"
     ></warning-text-message-dialog>
     ></warning-text-message-dialog>
+    <!-- audio-record-dialog -->
+    <audio-record-dialog
+      :record-id="recordId"
+      ref="AudioRecordDialog"
+    ></audio-record-dialog>
 
 
     <!-- 通话弹出层 -->
     <!-- 通话弹出层 -->
     <el-dialog
     <el-dialog
@@ -276,12 +282,18 @@ import {
 import FlvMedia from "../common/FlvMedia";
 import FlvMedia from "../common/FlvMedia";
 import StudentBreachDialog from "./StudentBreachDialog";
 import StudentBreachDialog from "./StudentBreachDialog";
 import WarningTextMessageDialog from "./WarningTextMessageDialog";
 import WarningTextMessageDialog from "./WarningTextMessageDialog";
+import AudioRecordDialog from "./audioRecord/AudioRecordDialog";
 import { formatDate, timeNumberToText } from "@/utils/utils";
 import { formatDate, timeNumberToText } from "@/utils/utils";
 import { mapState } from "vuex";
 import { mapState } from "vuex";
 
 
 export default {
 export default {
   name: "warning-detail",
   name: "warning-detail",
-  components: { FlvMedia, StudentBreachDialog, WarningTextMessageDialog },
+  components: {
+    FlvMedia,
+    StudentBreachDialog,
+    WarningTextMessageDialog,
+    AudioRecordDialog,
+  },
   data() {
   data() {
     return {
     return {
       recordId: this.$route.params.recordId,
       recordId: this.$route.params.recordId,
@@ -480,6 +492,9 @@ export default {
     toSendTextMsg() {
     toSendTextMsg() {
       this.$refs.WarningTextMessageDialog.open();
       this.$refs.WarningTextMessageDialog.open();
     },
     },
+    toSendAudioMsg() {
+      this.$refs.AudioRecordDialog.open();
+    },
     // video relative
     // video relative
     initSubscribeVideo() {
     initSubscribeVideo() {
       if (this.firstViewVideo.id) this.firstViewVideoReady = true;
       if (this.firstViewVideo.id) this.firstViewVideoReady = true;

+ 334 - 0
src/features/invigilation/RealtimeMonitoring/audioRecord/AudioRecordDialog.vue

@@ -0,0 +1,334 @@
+<template>
+  <el-dialog
+    class="audio-record-dialog"
+    :visible.sync="dialogVisible"
+    width="390px"
+    title="语音提醒"
+    :close-on-press-escape="false"
+    :close-on-click-modal="false"
+    append-to-body
+    @open="initData"
+  >
+    <div class="record-time" v-if="recordStep !== 2">
+      <p>{{ recordTimeStr }}</p>
+      <p>(最长2分钟)</p>
+    </div>
+    <div class="record-audio-play" v-else>
+      <div class="audio-player">
+        <div
+          :class="['audio-player-play', { 'player-playing': isPlaying }]"
+          @click="play"
+        ></div>
+        <div class="audio-player-progress">
+          <el-progress
+            :percentage="audioPlayProgress"
+            :show-text="false"
+            :stroke-width="8"
+            status="success"
+          ></el-progress>
+          <p class="audio-player-tips">点击试听录音</p>
+        </div>
+        <div class="audio-player-time">{{ audioCurPlayTime }}</div>
+      </div>
+      <audio
+        :src="audioUrl"
+        class="audio-audio"
+        ref="AudioAudio"
+        @timeupdate="audioCurrentTimeChange"
+        @ended="audioEnded"
+      ></audio>
+    </div>
+    <div class="record-footer" slot="footer">
+      <el-button type="primary" v-if="recordStep === 0" @click="start"
+        >开始录音</el-button
+      >
+      <el-button type="primary" v-if="recordStep === 1" @click="finish"
+        >完成录音</el-button
+      >
+
+      <el-button v-if="recordStep === 2" @click="restart">重新录音</el-button>
+      <el-button
+        type="primary"
+        v-if="recordStep === 2"
+        @click="confirm"
+        :disabled="isSubmit"
+        >确认录音</el-button
+      >
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { sendAudioWarningMsg } from "@/api/invigilation";
+import AudioRecord from "./audioRecord";
+
+function recordTimeToText(timeNumber) {
+  const MINUTE_TIME = 60 * 1000;
+  const SECOND_TIME = 1000;
+  let [minute, second] = [0, 0, 0, 0];
+  let residueTime = timeNumber;
+
+  if (residueTime >= MINUTE_TIME) {
+    minute = Math.floor(residueTime / MINUTE_TIME);
+    residueTime -= minute * MINUTE_TIME;
+  }
+  if (residueTime >= SECOND_TIME) {
+    second = Math.round(residueTime / SECOND_TIME);
+  }
+
+  return [minute, second]
+    .map((item) => ("00" + item).substr(("" + item).length))
+    .join(":");
+}
+
+export default {
+  name: "audio-record-dialog",
+  props: {
+    recordId: {
+      type: String,
+    },
+  },
+  data() {
+    return {
+      dialogVisible: false,
+      isSubmit: false,
+      recordStep: 0, // 0:未开始,1:开始录音,2:录音结束
+      recordReady: false,
+      recordLimitTime: 2 * 60 * 1000,
+      recordStartTime: 0,
+      recordEndTime: 0,
+      recordTime: 0,
+      recordTimeStr: "00:00",
+      audioUrl: "",
+      isPlaying: false,
+      audioPlayProgress: 0,
+      audioCurPlayTime: "00:00",
+      audioRecord: null,
+      audioBlob: null,
+      setT: null,
+    };
+  },
+  methods: {
+    initData() {
+      this.isSubmit = false;
+      this.recordStep = 0;
+      this.recordStartTime = 0;
+      this.recordEndTime = 0;
+      this.recordTime = 0;
+      this.recordTimeStr = "00:00";
+      this.audioUrl = "";
+      this.audioPlayProgress = 0;
+      this.audioCurPlayTime = "00:00";
+      this.audioBlob = null;
+      this.setT = null;
+    },
+    cancel() {
+      this.dialogVisible = false;
+    },
+    open() {
+      this.dialogVisible = true;
+    },
+    // audio play
+    play() {
+      if (this.$refs.AudioAudio.ended) {
+        this.$refs.AudioAudio.currentTime = 0;
+        this.$refs.AudioAudio.play();
+        this.isPlaying = true;
+        this.audioPlayProgress = 0;
+        this.audioCurPlayTime = "00:00";
+        return;
+      }
+
+      if (this.$refs.AudioAudio.paused) {
+        this.$refs.AudioAudio.play();
+        this.isPlaying = true;
+      } else {
+        this.$refs.AudioAudio.pause();
+        this.isPlaying = false;
+      }
+      console.dir(this.$refs.AudioAudio);
+    },
+    audioCurrentTimeChange() {
+      const { duration, currentTime } = this.$refs.AudioAudio;
+      const audioPlayProgress = (100 * currentTime) / duration;
+      this.audioPlayProgress = audioPlayProgress
+        ? audioPlayProgress > 100
+          ? 100
+          : audioPlayProgress
+        : 0;
+      this.audioCurPlayTime = recordTimeToText(currentTime * 1000);
+    },
+    audioEnded() {
+      this.isPlaying = false;
+    },
+    // audio record
+    updateRecordTime() {
+      this.recordTime = Date.now() - this.recordStartTime;
+      this.recordTimeStr = recordTimeToText(this.recordTime);
+    },
+    start() {
+      this.audioRecord = new AudioRecord({
+        onaudioprocess: this.updateRecordTime,
+      });
+      this.audioRecord.init((err) => {
+        if (err) {
+          this.recordReady = false;
+          this.$message.error({ message: err.msg, duration: 3000 });
+          return;
+        }
+        this.recordStartTime = Date.now();
+        this.audioRecord.start();
+        this.recordStep = 1;
+        this.setT = setTimeout(() => {
+          this.finish();
+        }, this.recordLimitTime);
+      });
+    },
+    finish() {
+      if (this.setT) clearTimeout(this.setT);
+
+      this.audioBlob = this.audioRecord.getAudioBlob();
+      console.log(this.audioBlob);
+      this.audioUrl = window.URL.createObjectURL(this.audioBlob);
+      this.updateRecordTime();
+      this.recordStep = 2;
+      this.audioRecord.recover();
+      console.log(this.recordTime);
+      console.log(this.recordTimeStr);
+    },
+    restart() {
+      this.initData();
+      this.start();
+    },
+    async confirm() {
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+
+      const fdata = new FormData();
+      fdata.append("content", this.audioBlob);
+      fdata.append("type", "audio");
+      fdata.append("examRecordId", this.recordId);
+      const result = await sendAudioWarningMsg(fdata).catch(() => {});
+
+      this.isSubmit = false;
+      if (!result) return;
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+  beforeDestroy() {},
+};
+
+// 参考文件:
+// https://blog.csdn.net/qq_42986378/article/details/81872679
+// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
+// https://developer.mozilla.org/zh-CN/docs/Web/API/AudioContext
+// https://developer.mozilla.org/zh-CN/docs/Web/API/AudioContext/createMediaStreamSource
+// https://developer.mozilla.org/zh-CN/docs/Web/API/AudioContext/createScriptProcessor
+// https://www.cnblogs.com/hustskyking/p/javascript-array.html
+// https://www.barretlee.com/blog/2014/02/20/cb-webAudio-listen/
+// https://www.barretlee.com/blog/2014/02/22/cb-webAudio-show-audio/
+
+// method
+// navigator.mediaDevices.getUserMedia => stream
+// new AudioContext() => context
+// context.createMediaStreamSource(stream) => audioInput
+// context.createScriptProcessor(4096, 1, 1) => recorder
+
+// start => () => {
+//   audioInput.connect(recorder);
+//   recorder.connect(context.destination);
+// }
+
+// recording
+// recorder.onaudioprocess => () => {audioData.push(e.inputBuffer.getChannelData(0))}
+
+// stop => () => {
+//   recorder.disconnect()
+// }
+</script>
+
+<style lang="scss">
+.record-time {
+  padding: 30px;
+  text-align: center;
+  > p:first-child {
+    font-size: 40px;
+    line-height: 1.1;
+    font-weight: 400;
+    color: #202b4b;
+    margin: 0;
+  }
+  > p:last-child {
+    font-size: 14px;
+    font-weight: 400;
+    color: #8c94ac;
+    line-height: 20px;
+    margin: 0;
+  }
+}
+.record-audio-play {
+  padding: 30px 0;
+
+  .audio-audio {
+    visibility: hidden;
+  }
+}
+.audio-player {
+  height: 64px;
+  padding: 16px 25px;
+  background: #f0f4f9;
+  border-radius: 10px;
+  position: relative;
+
+  &-play {
+    width: 32px;
+    height: 32px;
+    float: left;
+    background-image: url("../../../../assets/icon-play.png");
+    background-size: 100% 100%;
+    cursor: pointer;
+
+    &.player-playing {
+      background-image: url("../../../../assets/icon-play-act.png");
+    }
+    &:hover {
+      background-image: url("../../../../assets/icon-play-act.png");
+    }
+  }
+  &-progress {
+    margin-left: 40px;
+    margin-right: 40px;
+    padding-top: 12px;
+    position: relative;
+  }
+  &-tips {
+    position: absolute;
+    left: 0;
+    top: 24px;
+    font-size: 10px;
+    font-weight: 400;
+    color: #8c94ac;
+    line-height: 14px;
+  }
+  &-time {
+    position: absolute;
+    right: 25px;
+    top: 23px;
+    height: 18px;
+    font-size: 12px;
+    font-weight: 400;
+    color: #202b4b;
+    line-height: 18px;
+  }
+  .el-progress-bar__outer {
+    background: #dbe1e7;
+  }
+  .el-progress.is-success .el-progress-bar__inner {
+    background: #1cd1a1;
+  }
+}
+.record-footer {
+  text-align: center;
+}
+</style>

+ 35 - 47
src/features/invigilation/RealtimeMonitoring/audioRecode.js → src/features/invigilation/RealtimeMonitoring/audioRecord/audioRecord.js

@@ -1,33 +1,17 @@
-const getAdapterUserMedia = () => {
-  if (navigator.mediaDevices === undefined) navigator.mediaDevices = {};
-
-  if (navigator.mediaDevices.getUserMedia === undefined) {
-    navigator.mediaDevices.getUserMedia = (constraints) => {
-      const getUserMedia =
-        navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
-
-      if (!getUserMedia)
-        return Promise.reject(
-          new Error("getUserMedia is not implemented in this browser")
-        );
-
-      return new Promise((resolve, reject) => {
-        getUserMedia.call(navigator, constraints, resolve, reject);
-      });
-    };
-  }
-
-  return navigator.mediaDevices.getUserMedia;
-};
-
-class AudioRecode {
+class AudioRecord {
   constructor(opt = {}) {
   constructor(opt = {}) {
-    const defaultOptions = { sampleBits: 8, sampleRate: 44100 / 6 };
+    const defaultOptions = {
+      sampleBits: 8,
+      sampleRate: 44100 / 6,
+      onaudioprocess: function () {},
+    };
     this.options = Object.assign(defaultOptions, opt);
     this.options = Object.assign(defaultOptions, opt);
+    console.log(this.options);
+    this.onaudioprocess = this.options.onaudioprocess;
     this.size = 0;
     this.size = 0;
     this.audioBuffer = [];
     this.audioBuffer = [];
     this.inputSampleRate = 0;
     this.inputSampleRate = 0;
-    this.inputSampleBits = 2;
+    this.inputSampleBits = 16;
     this.outputSampleRate = this.options.sampleRate;
     this.outputSampleRate = this.options.sampleRate;
     this.outputSampleBits = this.options.sampleBits;
     this.outputSampleBits = this.options.sampleBits;
     this.audioContext = null;
     this.audioContext = null;
@@ -63,7 +47,7 @@ class AudioRecode {
     const sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
     const sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
     const sampleBits = Math.min(this.inputSampleBits, this.outputSampleBits);
     const sampleBits = Math.min(this.inputSampleBits, this.outputSampleBits);
     const compressData = this.compress();
     const compressData = this.compress();
-    const audioCompressDataLength = compressData.length * (sampleRate / 8);
+    const audioCompressDataLength = compressData.length * (sampleBits / 8);
     const audioArrayBuffer = new ArrayBuffer(44 + audioCompressDataLength);
     const audioArrayBuffer = new ArrayBuffer(44 + audioCompressDataLength);
     let audioDataView = new DataView(audioArrayBuffer);
     let audioDataView = new DataView(audioArrayBuffer);
     const channelCount = 1;
     const channelCount = 1;
@@ -156,25 +140,29 @@ class AudioRecode {
     return { msg, code: errorContent };
     return { msg, code: errorContent };
   }
   }
 
 
-  async init() {
-    const getUserMedia = getAdapterUserMedia();
-    let error = null;
-    const stream = await getUserMedia({ audio: true }).catch((err) => {
-      error = err;
-    });
-
-    if (error) {
-      return Promise.reject(this.mediaError(error));
+  async init(callback) {
+    if (!navigator.getUserMedia) {
+      return callback({ msg: "当前浏览器不支持录音功能。" });
     }
     }
-
-    this.audioContext = new AudioContext();
-    this.inputSampleRate = this.audioContext.sampleRate;
-    this.media = this.audioContext.createMediaStreamSource(stream);
-    this.recorder = this.audioContext.createScriptProcessor(4096, 1, 1);
-    //音频采集
-    this.recorder.onaudioprocess = (e) => {
-      this.input(e.inputBuffer.getChannelData(0));
-    };
+    navigator.getUserMedia(
+      { audio: true },
+      (stream) => {
+        this.audioContext = new window.AudioContext();
+        this.inputSampleRate = this.audioContext.sampleRate;
+        console.log(this.inputSampleRate);
+        this.media = this.audioContext.createMediaStreamSource(stream);
+        this.recorder = this.audioContext.createScriptProcessor(4096, 1, 1);
+        //音频采集
+        this.recorder.onaudioprocess = (e) => {
+          this.input(e.inputBuffer.getChannelData(0));
+          this.onaudioprocess();
+        };
+        callback();
+      },
+      (err) => {
+        return callback(this.mediaError(err));
+      }
+    );
   }
   }
 
 
   start() {
   start() {
@@ -184,6 +172,7 @@ class AudioRecode {
 
 
   stop() {
   stop() {
     this.recorder.disconnect();
     this.recorder.disconnect();
+    this.media.disconnect();
   }
   }
 
 
   getAudioBlob() {
   getAudioBlob() {
@@ -191,14 +180,13 @@ class AudioRecode {
     return this.encode();
     return this.encode();
   }
   }
 
 
-  restart() {
+  recover() {
     this.size = 0;
     this.size = 0;
     this.audioBuffer = [];
     this.audioBuffer = [];
     this.audioContext = null;
     this.audioContext = null;
     this.media = null;
     this.media = null;
     this.recorder = null;
     this.recorder = null;
-    this.start();
   }
   }
 }
 }
 
 
-export default AudioRecode;
+export default AudioRecord;