<template>
  <div>
    <video
      id="video"
      ref="video"
      :width="width"
      :height="height"
      autoplay
    ></video>
    <div
      v-if="showRecognizeButton"
      style="position: absolute; width: 400px; text-align: center; margin-top: -50px; color: #232323;"
    >
      <button
        :class="['verify-button', disableSnap && 'disable-verify-button']"
        :disabled="disableSnap"
        @click="snap"
      >
        {{ msg }}
      </button>
    </div>
  </div>
</template>

<script>
import MD5 from "js-md5";
import { mapState as globalMapState } from "vuex";
import { createNamespacedHelpers } from "vuex";
const { mapState, mapMutations } = createNamespacedHelpers("examingHomeModule");
import { chromeUA } from "@/utils/ua.js";

export default {
  name: "FaceRecognition",
  props: {
    width: { type: String, default: "400" },
    height: { type: String, default: "300" },
    showRecognizeButton: Boolean,
    closeCamera: Boolean, // optional
  },
  data() {
    return { disableSnap: true, msg: "开始识别" };
  },
  computed: {
    ...globalMapState(["user"]),
    ...mapState(["snapNow"]),
  },
  watch: {
    snapNow(val) {
      if (val) {
        if (!this.lastSnapTime || Date.now() - this.lastSnapTime > 20 * 1000) {
          this.lastSnapTime = Date.now();
          this.snapTimer();
        } else {
          // this.serverLog(
          //   "debug/S-002001",
          //   "上次的抓拍未超过1分钟，本次抓拍指令取消"
          // );
          window._hmt.push([
            "_trackEvent",
            "摄像头框",
            "上次的抓拍未超过1分钟，本次抓拍指令取消",
          ]);
          this.decreaseSnapCount();
        }
        this.toggleSnapNow();
      }
    },
    closeCamera: function(newValue) {
      if (newValue) {
        console.log("关闭摄像头");
        window.__stream = null;
        if (this.$refs.video.srcObject) {
          this.$refs.video.srcObject.getTracks().forEach(function(track) {
            track.stop();
          });
          this.$refs.video.srcObject = null;
        }
      } else {
        this.openCamera();
      }
    },
  },
  async mounted() {
    window.__stream = null;
    this.openCamera();

    // this.checkFaceDetectorTimeout = setTimeout(() => {
    //   this.nativeFaceDetectorStats();
    // }, 60 * 1000);
  },
  beforeDestroy() {
    clearTimeout(this.retrySnapTimeout);
    clearTimeout(this.showSnapResultTimeout);
    // clearTimeout(this.checkFaceDetectorTimeout);
    window.__stream = null;
    if (this.$refs.video.srcObject) {
      this.$refs.video.srcObject.getTracks().forEach(function(track) {
        track.stop();
      });
      this.$refs.video.srcObject = null;
    }
  },
  methods: {
    ...mapMutations(["toggleSnapNow", "decreaseSnapCount"]),
    async openCamera() {
      const _openStartTime = Date.now();
      const video = this.$refs.video;
      if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
        try {
          console.log("启动摄像头");
          this.logger({ action: "启动摄像头" });
          console.log({
            getSupportedConstraints:
              navigator.mediaDevices.getSupportedConstraints &&
              navigator.mediaDevices.getSupportedConstraints(),
          });
          this.logger({
            action: "摄像头打开",
            detail: "摄像头getSupportedConstraints",
            getSupportedConstraints:
              navigator.mediaDevices.getSupportedConstraints &&
              navigator.mediaDevices.getSupportedConstraints(),
          });
          const stream = await navigator.mediaDevices.getUserMedia({
            video: {
              facingMode: "user",
              aspectRatio: 4 / 3,
              resizeMode: "crop-and-scale",
              width: 640,
              height: 480,
            },
          });
          if (stream) {
            {
              const vt0 = stream.getVideoTracks()[0];
              if (
                vt0 &&
                vt0.getCapabilities &&
                vt0.getConstraints &&
                vt0.getSettings
              ) {
                console.log({
                  getCapabilities: vt0.getCapabilities && vt0.getCapabilities(),
                  getConstraints: vt0.getConstraints && vt0.getConstraints(),
                  getSettings: vt0.getSettings && vt0.getSettings(),
                });
              } else {
                console.log("stream.getVideoTracks()[0] failed");
              }
            }
            video.srcObject = stream;
            window.__stream = stream;
            try {
              try {
                await video.play();
              } catch (error) {
                if (error.name == "AbortError") {
                  console.log("AbortError and retry");
                  await video.play();
                  this.logger({
                    action: "摄像头没有正常启用: AbortError 重试成功",
                  });
                  window._hmt.push([
                    "_trackEvent",
                    "摄像头框",
                    "摄像头状态",
                    "摄像头没有正常启用: AbortError 重试成功",
                  ]);
                } else {
                  throw error;
                }
              }
              this.disableSnap = false;
              const _openEndTime = Date.now();
              window._hmt.push([
                "_trackEvent",
                "摄像头框",
                "摄像头打开耗时",
                Number(
                  Math.floor((_openEndTime - _openStartTime) / 1000)
                ).toPrecision(1) + "秒",
              ]);
              console.log(
                "摄像头打开耗时",
                Math.floor((_openEndTime - _openStartTime) / 1000) + "秒"
              );
              this.logger({
                action:
                  "摄像头打开耗时" +
                  Math.floor((_openEndTime - _openStartTime) / 1000) +
                  "秒",
              });
            } catch (error) {
              console.log("摄像头没有正常启用", error);
              this.logger({
                action: "摄像头没有正常启用",
                detail: error,
                errorName: error.name,
                errorMessage: error.message,
                errorStringify: JSON.stringify(error),
              });
              this.$Message.error({
                content: "摄像头没有正常启用: " + error,
                duration: 15,
                closable: true,
              });
              window._hmt.push([
                "_trackEvent",
                "摄像头框",
                "摄像头状态",
                "摄像头没有正常启用: " + error,
              ]);
            }
          } else {
            this.logger({
              action: "摄像头打开失败",
              detail: "没有可用的视频流",
            });
            this.$Message.error({
              content: "没有可用的视频流",
              duration: 15,
              closable: true,
            });
            window._hmt.push([
              "_trackEvent",
              "摄像头框",
              "摄像头状态",
              "没有可用的视频流",
            ]);
          }
        } catch (error) {
          console.log("无法启用摄像头", error);
          this.logger({
            action: "摄像头打开失败",
            detail: "无法启用摄像头",
            error: error.message,
            // getSupportedConstraints:
            //   navigator.mediaDevices.getSupportedConstraints &&
            //   navigator.mediaDevices.getSupportedConstraints(),
          });
          let errMsg;
          if (error.name || error.message) {
            errMsg = `${error.name} ${error.message}`;
          } else {
            errMsg = error;
          }

          if (error.name === "NotReadableError") {
            this.$Message.error({
              content: "无法启用摄像头: " + error.name + " 请重试！",
              duration: 15,
              closable: true,
            });
          } else if (
            error.name === "NotFoundError" ||
            error.name === "DevicesNotFoundError"
          ) {
            this.$Message.error({
              content:
                "无法启用摄像头: " +
                error.name +
                " 没有找到合适的摄像头！请重试或更换摄像头！",
              duration: 15,
              closable: true,
            });
          } else {
            this.$Message.error({
              content: "无法启用摄像头: " + error.name + errMsg,
              duration: 15,
              closable: true,
            });
          }

          const errorMsgLog =
            errMsg + (typeof errMsg === "object" ? JSON.stringify(errMsg) : "");
          this.logger({
            action: "摄像头打开失败",
            detail: "无法启用摄像头",
            error: errorMsgLog,
            // getSupportedConstraints:
            //   navigator.mediaDevices.getSupportedConstraints &&
            //   navigator.mediaDevices.getSupportedConstraints(),
          });
          window._hmt.push([
            "_trackEvent",
            "摄像头框",
            "摄像头状态",
            "无法启用摄像头" + errorMsgLog,
          ]);
        }
      } else {
        this.logger({
          action: "摄像头打开失败",
          detail: "没有找到可用的摄像头",
        });
        this.$Message.error({
          content: "没有找到可用的摄像头",
          duration: 15,
          closable: true,
        });
        window._hmt.push([
          "_trackEvent",
          "摄像头框",
          "摄像头状态",
          "没有找到可用的摄像头",
        ]);
      }
    },
    async snapTimer() {
      try {
        this.logger({ action: "定时抓拍开始" });
        const examRecordDataId = this.$route.params.examRecordDataId;
        const captureBlob = await this.getSnapShot({ compareSync: false });
        this.logger({ action: "抓拍照片的大小：" + captureBlob.size });
        this.videoStartPlay();
        console.log("抓拍照片的大小：" + captureBlob.size);
        if (captureBlob.size < 48 * 48 || captureBlob.size >= 2 * 1024 * 1024) {
          // 经查以前记录，不完整图片均为8192大小。此处设置小于10KB的图片为未抓拍成功
          // 检查百度统计的记录后，这里的图片大小可能小于8192，也可能是有效的数据，所以降低图片大小的要求为face++的要求
          window._hmt.push([
            "_trackEvent",
            "摄像头框",
            "定时抓拍照片大小异常" + chromeUA.major,
            captureBlob.size,
          ]);
          this.logger({
            action: "摄像头异常",
            detail: "定时抓拍照片大小异常",
            error: captureBlob.size,
          });
          throw "定时抓拍照片大小异常";
        }
        // this.serverLog("debug/S-004001", "抓拍照片的大小：" + captureBlob.size);
        const startTime = Date.now();
        const [captureFilePath, signIdentifier] = await this.uploadToServer(
          captureBlob
        );
        const endTime = Date.now();
        this.logger({
          action: "定时抓拍上传耗时：" + (endTime - startTime),
        });
        await this.faceCompare(
          captureFilePath,
          signIdentifier,
          examRecordDataId
        );
        this.logger({
          action: "定时抓拍比对耗时：" + (Date.now() - endTime),
        });
        console.log("定时抓拍流程成功", signIdentifier);
      } catch (error) {
        console.log("定时抓拍流程失败", error);
        this.logger({
          action: "定时抓拍流程失败",
          detail: error,
        });
        window._hmt.push([
          "_trackEvent",
          "摄像头框",
          "定时抓拍流程失败" + error,
          (this.lastSnapTime ? "(非初次抓拍)" : "") + "将再次抓拍",
        ]);
        this.retrySnapTimeout = setTimeout(() => {
          this.toggleSnapNow();
        }, 60 * 1000);
      } finally {
        this.videoStartPlay();
        this.decreaseSnapCount();
      }
    },
    videoStartPlay() {
      const video = this.$refs.video;
      video && video.play();
    },
    async snap() {
      // TODO: chrome 70. FaceDetector检测人脸
      // var canvas = document.createElement("canvas");
      // canvas.width = 220;
      // canvas.height = 165;

      // var context = canvas.getContext("2d");
      // context.drawImage(this.$refs.video, 0, 0, 220, 165);
      // var f = new FaceDetector();
      // const v = await f.detect(canvas);
      // console.log(v);
      // return;

      this.$Message.destroy();
      try {
        this.disableSnap = true;
        // console.log("disableSnap: " + this.disableSnap);
        // await new Promise(resolve =>
        //   setTimeout(() => {
        //     console.log(new Date());
        //     resolve();
        //   }, 3000)
        // );
        // return;
        // if(this.disableSnap) return; // 避免界面没有更新。
        this.msg = "拍照中...";
        const captureBlob = await this.getSnapShot({ compareSync: true });
        console.log("抓拍照片大小", captureBlob.size);
        if (captureBlob.size < 48 * 48 || captureBlob.size >= 2 * 1024 * 1024) {
          this.$Message.error({
            content: "抓拍照片太小！",
            duration: 15,
            closable: true,
          });
          // 经查以前记录，不完整图片均为8192大小。此处设置小于10KB的图片为未抓拍成功
          // 检查百度统计的记录后，这里的图片大小可能小于8192，也可能是有效的数据，所以降低图片大小的要求为face++的要求
          window._hmt.push([
            "_trackEvent",
            "摄像头框",
            "抓拍照片大小异常" + chromeUA.major,
            captureBlob.size,
          ]);
          throw "抓拍照片大小异常";
        }
        this.videoStartPlay();
        this.msg = "上传照片中...";
        console.log(this.msg);
        const [captureFilePath, signIdentifier] = await this.uploadToServer(
          captureBlob
        );
        this.msg = "人脸比对中...";
        console.log(this.msg);
        await this.faceCompareSync(captureFilePath, signIdentifier);
        console.log("人脸比对成功");
      } catch (error) {
        console.log("同步照片比对流程失败");
        throw error;
      } finally {
        this.videoStartPlay();
        this.msg = "开始识别";
        this.disableSnap = false;
      }
    },
    async getSnapShot({ compareSync }) {
      return new Promise((resolve, reject) => {
        const video = this.$refs.video;
        if (video.readyState !== 4 || !video.srcObject.active) {
          this.$Message.error({
            content: "摄像头没有正常启用",
            duration: 5,
            closable: true,
          });

          window._hmt.push([
            "_trackEvent",
            "摄像头框",
            "摄像头状态",
            "摄像头没有正常启用" +
              (!compareSync && this.lastSnapTime ? "-退出(非初次抓拍)" : ""),
          ]);
          reject("摄像头没有正常启用");
          if (!compareSync) {
            this.logout(
              "?LogoutReason=" +
                "摄像头没有正常启用" +
                (!compareSync && this.lastSnapTime ? "-退出(非初次抓拍)" : "")
            );
          }
          return;
        }
        video.pause();
        var canvas = document.createElement("canvas");
        canvas.width = 220;
        canvas.height = 165;

        var context = canvas.getContext("2d");
        context.drawImage(video, 0, 0, 220, 165);

        canvas.toBlob(resolve, "image/png", 0.95);
      });
    },
    async uploadToServer(captureBlob) {
      async function blobToArray(blob) {
        return new Promise(resolve => {
          var reader = new FileReader();
          reader.addEventListener("loadend", function() {
            // reader.result contains the contents of blob as a typed array
            resolve(reader.result);
          });
          reader.readAsArrayBuffer(blob);
        });
      }
      //保存抓拍照片到服务器
      let resultUrl, signIdentifier;
      try {
        const buffer = await blobToArray(captureBlob);

        // console.log(buffer);
        // var view1 = new Uint8Array(buffer);
        // console.log(buffer[0], buffer[1], buffer[429721]);
        // const fileMd5 = MD5(buffer);
        const fileMd5Base64 = window.btoa(
          String.fromCharCode(...MD5.digest(buffer))
        );
        // console.log(fileMd5);
        // console.log(fileMd5, fileMd5Base64);

        const params = new URLSearchParams();
        params.append("fileSuffix", "png");
        // params.append("fileMd5", fileMd5);
        const res = await this.$http.get(
          "/api/ecs_oe_student/examControl/getCapturePhotoYunSign?" + params
        );

        // console.log(res);

        // let myHeaders = new Headers();
        // for (let [k, v] of Object.entries(res.data.headers)) {
        //   // console.log(k, v);
        //   if (k.includes("tion") || k.includes("Date") || k.includes("MD5")) {
        //     if (k === "Date") k = "x-date";
        //     myHeaders.append(k, v);
        //   }
        let myFormData = new FormData();
        for (let [k, v] of Object.entries(res.data.formParams)) {
          myFormData.append(k, v);
        }
        myFormData.append("file", captureBlob);
        // myFormData.toBlob();
        // let myHeaders = new Headers();
        // myHeaders.append("Content-MD5", fileMd5Base64);

        try {
          const res2 = await fetch(res.data.formUrl, {
            method: "POST",
            // headers: myHeaders,
            body: myFormData,
          });
          console.log(res2, {
            "res2.headers": res2.headers,
            'res2.headers.get("Content-MD5")': res2.headers.get("Content-MD5"),
            'res2.headers.get("Content-Type")': res2.headers.get(
              "Content-Type"
            ),
          });
          // throw "eee";
          if (!res2.ok) {
            throw res2.status;
          }
          if (res2.headers.get("Content-MD5") != fileMd5Base64) {
            window._hmt.push([
              "_trackEvent",
              "摄像头框",
              "抓拍照片保存失败--alioss content-md5 mismatch",
            ]);
            throw "图片校验失败";
          }
        } catch (error) {
          window._hmt.push([
            "_trackEvent",
            "摄像头框",
            "抓拍照片保存失败--upyun",
            error,
          ]);
          throw error;
        }

        // console.log(response);
        resultUrl = res.data.accessUrl;
        signIdentifier = res.data.signIdentifier;
        // this.serverLog("debug/S-005001", "抓拍照片保存成功：");
        window._hmt.push(["_trackEvent", "摄像头框", "抓拍照片保存成功"]);
      } catch (e) {
        console.log(e);
        // this.serverLog("debug/S-006001", "抓拍照片保存失败");
        window._hmt.push([
          "_trackEvent",
          "摄像头框",
          "保存抓拍照片到服务器失败！",
        ]);
        this.$Message.error({
          content: "抓拍照片保存失败！",
          duration: 15,
          closable: true,
        });
        throw "抓拍照片保存失败！";
      }

      // let UPYUN_URL;
      // try {
      //   UPYUN_URL = (await this.$http.get("/api/ecs_oe_student_face/upyun"))
      //     .data.downloadPrefix;
      // } catch (error) {
      //   this.$Message.error({ content: "获取照片下载前缀失败！", duration: 15,  closable: true});
      //   throw "获取照片下载前缀失败！";
      // }
      return [resultUrl, signIdentifier];
    },
    async faceCompareSync(captureFilePath, signIdentifier) {
      try {
        const res = await this.$http.post(
          "/api/ecs_oe_student_face/examCaptureQueue/compareFaceSync?signIdentifier=" +
            signIdentifier +
            "&fileUrl=" +
            encodeURIComponent(captureFilePath)
        );

        this.$emit("on-recognize-result", {
          error: null,
          pass: res.data.isPass,
          stranger: res.data.isStranger,
        });
      } catch (e) {
        console.log(e);
        // this.$Message.error(e.message);
        throw "同步照片比较失败！";
      }
    },
    async faceCompare(captureFilePath, signIdentifier, examRecordDataId) {
      try {
        let cameraInfos;
        let hasVirtualCamera = false;
        if (typeof nodeRequire != "undefined") {
          try {
            var fs = window.nodeRequire("fs");
            if (fs.existsSync("multiCamera.exe")) {
              await new Promise((resolve, reject) => {
                window.nodeRequire("node-cmd").get("multiCamera.exe", () => {
                  try {
                    cameraInfos = fs.readFileSync("CameraInfo.txt", "utf-8");
                    if (cameraInfos && cameraInfos.trim()) {
                      cameraInfos = cameraInfos.trim();
                      cameraInfos = cameraInfos.replace(/\r\n/g, "");
                      cameraInfos = cameraInfos.replace(/\n/g, "");
                      console.log(cameraInfos);
                      this.serverLog("debug/S-001001", cameraInfos);
                    }
                    if (cameraInfos.includes('""')) {
                      hasVirtualCamera = true;
                    }
                    // multiCamera.exe 1.0.1
                    if (cameraInfos.includes("cameraInfo")) {
                      cameraInfos = JSON.stringify(
                        JSON.parse(cameraInfos).cameraInfo
                      );
                    }
                    resolve();
                  } catch (error) {
                    window._hmt.push([
                      "_trackEvent",
                      "摄像头框",
                      "虚拟摄像头-读取摄像头列表失败",
                    ]);
                    reject("读取摄像头列表失败");
                  }
                });
              });
            }
          } catch (error) {
            console.log(error);
          }
        }

        let body = {
          fileUrl: captureFilePath,
          signIdentifier,
          examRecordDataId,
        };

        if (cameraInfos) {
          body.cameraInfos = cameraInfos;
          body.hasVirtualCamera = hasVirtualCamera;

          this.logger({
            action: "抓拍照片详细日志",
            fileUrl: captureFilePath,
            signIdentifier,
            examRecordDataId,
            cameraInfos,
            hasVirtualCamera,
          });
        }
        const res = await this.$http.post(
          "/api/ecs_oe_student_face/examCaptureQueue/uploadExamCapture",
          body
        );
        const fileName = res.data;
        try {
          await this.showSnapResult(fileName, examRecordDataId);
        } catch (error) {
          this.$Message.error({
            content: "设置获取抓拍结果失败！",
            duration: 15,
            closable: true,
          });
        }
      } catch (e) {
        console.log(e);
        window._hmt.push([
          "_trackEvent",
          "摄像头框",
          "faceCompare失败",
          e.response ? e.response.data.desc : e,
        ]);
        // this.$Message.error(e.message);
        throw "异步比较抓拍照片失败";
      }
    },
    async showSnapResult(fileName, examRecordDataId) {
      if (!fileName) return; // 交卷后提交照片会得不到照片名称
      if (this.$route.name !== "OnlineExamingHome") {
        // 非考试页，不显示结果，也不继续查询
        return;
      }

      try {
        // 获取抓拍结果
        const snapRes =
          (await this.$http.get(
            "/api/ecs_oe_student_face/examCaptureQueue/getExamCaptureResult?fileName=" +
              fileName +
              "&examRecordDataId=" +
              examRecordDataId
          )).data || {};
        if (snapRes.isCompleted) {
          if (snapRes.isStranger) {
            this.$Message.error({
              content: "请独立完成考试",
              duration: 5,
              closable: true,
            });
          } else if (!snapRes.isPass) {
            this.$Message.error({
              content: "请调整坐姿，诚信考试",
              duration: 5,
              closable: true,
            });
          }
        } else {
          this.showSnapResultTimeout = setTimeout(
            this.showSnapResult.bind(this, fileName, examRecordDataId),
            30 * 1000
          );
        }
      } catch (e) {
        console.log(e);
        if (this.$route.name !== "OnlineExamingHome") {
          // 非考试页，不显示结果，也不继续查询
          return;
        }
        this.$Message.error(e.message);
        throw e.message;
      }
    },
    async nativeFaceDetectorStats() {
      // 如果不存在 window.FaceDetector ，则不统计。chrome 76+ 在win7上是有window.FaceDetector的，但是不能执行成功。
      if (!window.FaceDetector) {
        window._hmt.push([
          "_trackEvent",
          "摄像头框",
          "native FaceDetector",
          "没有能力",
        ]);
        return;
      }

      try {
        var canvas = document.createElement("canvas");
        canvas.width = 220;
        canvas.height = 165;

        var context = canvas.getContext("2d");
        context.drawImage(this.$refs.video, 0, 0, 220, 165);
        var f = new window.FaceDetector();
        const v = await f.detect(canvas);
        console.log("nativeFaceDetector", v);
        window._hmt.push([
          "_trackEvent",
          "摄像头框",
          "native FaceDetector",
          "成功",
        ]);
      } catch (e) {
        console.log(e);
        window._hmt.push([
          "_trackEvent",
          "摄像头框",
          "native FaceDetector",
          "失败",
        ]);
      }
    },
  },
};
</script>

<style scoped>
.verify-button {
  font-size: 16px;
  background-color: #ffcc00;
  display: inline-block;
  padding: 6px 16px;
  border-radius: 6px;
}

.verify-button:hover {
  color: #444444;
  cursor: pointer;
}

.disable-verify-button {
  background-color: #f7f7f7;
  color: #c5c8ce;
}

.disable-verify-button:hover {
  cursor: not-allowed;
  color: #c5c8ce;
}
</style>
