123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588 |
- <script setup lang="ts">
- import MD5 from "js-md5";
- import { onMounted, watchEffect } from "vue";
- import { getMediaStream } from "@/utils/camera";
- import { httpApp } from "@/plugins/axiosApp";
- import { showLogout } from "@/utils/utils";
- import { getCapturePhotoYunSign, saveCapturePhoto } from "@/api/login";
- import { execLocal, fileExists } from "@/utils/nativeMethods";
- /**
- * 上层通过showRecognizeButton来控制是否是同步比对
- *
- * 同步比对通过onRecognizeResult得到人脸比对结果
- *
- * 异步比对通过snapId来控制是否该进行比对,什么时候进行,以什么频率频率进行,错误处理,均由上层控制
- * 异步比对同时传递一个snapId(time),供上层识别和计数
- * 可能存在多个异步比对的任务同时进行
- */
- const {
- width = 400,
- height = 300,
- snapId = 0,
- examRecordDataId = -1,
- showRecognizeButton = false,
- } = defineProps<{
- width: string;
- height: string;
- showRecognizeButton: boolean;
- snapId?: number;
- examRecordDataId?: number;
- }>();
- const emit = defineEmits<{
- (
- e: "on-recognize-result",
- v: { isPassed: boolean; isStranger: boolean }
- ): void;
- (
- e: "on-async-recognize-result",
- v: { hasError: boolean; fileName: string }
- ): void;
- }>();
- let snapBtnDisabled = $ref(true);
- let btnText = $ref("开始识别");
- watchEffect(() => {
- if (snapId) {
- void snapAsync();
- }
- });
- onMounted(async () => {
- await openCamera();
- });
- const video = $ref<HTMLVideoElement>();
- async function openCamera() {
- const _openStartTime = Date.now();
- const stream = await getMediaStream();
- video.srcObject = stream;
- try {
- await video.play();
- } catch (error) {
- if (error instanceof Error) {
- if (error.name == "AbortError") {
- logger({
- cnl: ["server"],
- act: "video.paly",
- dtl: "AbortError and retry",
- });
- await video.play();
- logger({
- cnl: ["server"],
- act: "摄像头没有正常启用: AbortError 重试成功",
- });
- } else if (error.name == "NotSupportedError") {
- logger({
- cnl: ["server"],
- act: "摄像头没有正常启用",
- ejn: JSON.stringify(error),
- ext: {
- errorName: error.name,
- errorMessage: error.message,
- errorStack: error.stack,
- },
- });
- $message.error("摄像头没有正常启用: " + error);
- } else {
- throw error;
- }
- } else {
- logger({
- cnl: ["server"],
- act: "video.play",
- dtl: "not an Error",
- stk: error + "",
- });
- }
- throw error;
- }
- snapBtnDisabled = false;
- const _openEndTime = Date.now();
- logger({
- cnl: ["server"],
- act: "摄像头打开耗时",
- ext: { cost: _openEndTime - _openStartTime },
- });
- }
- async function videoStartPlay() {
- if (video && video.paused) {
- await video.play().catch((e) => {
- if (!(e instanceof Error)) {
- logger({
- cnl: ["server"],
- act: "restart video play error",
- dtl: "not an Error",
- stk: e + "",
- });
- } else {
- logger({
- cnl: ["server"],
- act: "restart video play error",
- stk: e.stack,
- ejn: JSON.stringify(e),
- });
- }
- throw e;
- });
- }
- }
- //#region 同步人脸比对
- async function snapSync() {
- logger({
- cnl: ["server"],
- act: "同步人脸比对",
- dtl: "点击开始识别按钮",
- });
- $message.destroyAll();
- try {
- snapBtnDisabled = true;
- btnText = "拍照中...";
- logger({ cnl: ["server"], lvl: "debug", act: btnText });
- const captureBlob = await getSnapShot(true);
- if (!(captureBlob instanceof Blob)) return;
- logger({
- cnl: ["server"],
- lvl: "debug",
- act: "getSnapShot",
- ext: { blobSize: captureBlob.size },
- });
- if (captureBlob.size < 48 * 48 || captureBlob.size >= 2 * 1024 * 1024) {
- $message.error("抓拍照片太小!");
- logger({
- cnl: ["server"],
- act: "摄像头异常",
- dtl: "抓拍照片大小异常",
- ext: { blobSize: captureBlob.size },
- });
- throw new Error("抓拍照片大小异常");
- }
- btnText = "上传照片中...";
- logger({ cnl: ["server"], lvl: "debug", act: btnText });
- const [captureFilePath, signIdentifier] = await uploadToServer(captureBlob);
- btnText = "人脸比对中...";
- await faceCompareSync(captureFilePath, signIdentifier);
- logger({
- cnl: ["server"],
- act: "同步比对照片详细日志",
- ext: {
- fileUrl: captureFilePath,
- signIdentifier,
- },
- });
- } catch (error) {
- console.log("同步照片比对流程失败");
- throw error;
- } finally {
- btnText = "开始识别";
- // 避免人脸识别功能被大量重复点击
- await new Promise((resolve) => setTimeout(resolve, 3000));
- snapBtnDisabled = false;
- }
- }
- async function getSnapShot(compareSync: boolean): Promise<Blob | unknown> {
- return new Promise((resolve, reject) => {
- if (video.readyState !== 4 || !(video.srcObject as MediaStream).active) {
- $message.error("摄像头没有正常启用");
- logger({
- cnl: ["server"],
- pgu: "AUTO",
- act: "getSnapShot",
- dtl: "摄像头没有正常启用",
- });
- reject("摄像头没有正常启用");
- if (!compareSync) {
- showLogout("摄像头没有正常启用");
- }
- return;
- }
- // video.pause();
- const canvas = document.createElement("canvas");
- canvas.width = 220;
- canvas.height = 165;
- const context = canvas.getContext("2d");
- context?.drawImage(video, 0, 0, 220, 165);
- canvas.toBlob((blob) => resolve(blob!), "image/png", 0.95);
- })
- // .finally(() => void videoStartPlay()); // finally 会在返回前执行,满足我们的要求
- }
- // 用来比对两次抓拍照片的md5是否一样
- let __previousPhotoMD5 = "";
- async function uploadToServer(captureBlob: Blob): Promise<[string, string]> {
- async function blobToArray(blob: Blob): Promise<ArrayBuffer> {
- 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 as ArrayBuffer);
- });
- reader.readAsArrayBuffer(blob);
- });
- }
- //保存抓拍照片到服务器
- let resultUrl, signIdentifier;
- try {
- const buffer = await blobToArray(captureBlob);
- const fileMd5Base64 = window.btoa(
- String.fromCharCode(...MD5.digest(buffer))
- );
- if (fileMd5Base64 === __previousPhotoMD5) {
- logger({
- cnl: ["server"],
- pgu: "AUTO",
- act: "uploadToServer",
- key: "抓拍照片异常",
- stk: "两次fileMd5Base64一样,疑似摄像头卡住",
- });
- }
- __previousPhotoMD5 = fileMd5Base64;
- const res = await getCapturePhotoYunSign({ fileSuffix: "png" });
- try {
- const saveRes = await saveCapturePhoto(
- res.data.formUrl,
- res.data.formParams,
- { file: captureBlob }
- );
- if (saveRes.headers["content-md5"] != fileMd5Base64) {
- logger({
- cnl: ["server"],
- dtl: "抓拍照片保存失败--alioss content-md5 mismatch",
- ext: {
- fileMd5Base64,
- "content-md5": saveRes.headers["content-md5"],
- },
- });
- throw new Error("图片校验失败");
- }
- } catch (error) {
- logger({
- cnl: ["server"],
- dtl: "抓拍照片保存失败",
- possibleError: error,
- });
- throw error;
- }
- // console.log(response);
- resultUrl = res.data.accessUrl;
- signIdentifier = res.data.signIdentifier;
- logger({
- cnl: ["server"],
- dtl: "抓拍照片保存成功",
- ext: { resultUrl, fileMd5Base64 },
- });
- } catch (e) {
- console.log(e);
- logger({
- cnl: ["server"],
- pgu: "AUTO",
- dtl: "保存抓拍照片到服务器失败!",
- possibleError: e,
- });
- $message.error("抓拍照片保存失败!");
- throw new Error("抓拍照片保存失败!");
- }
- return [resultUrl, signIdentifier];
- }
- async function faceCompareSync(
- captureFilePath: string,
- signIdentifier: string
- ) {
- try {
- logger({ cnl: ["server"], act: "同步比对开始" });
- const res = await httpApp.post(
- "/api/ecs_oe_student_face/examCaptureQueue/compareFaceSync?signIdentifier=" +
- signIdentifier +
- "&fileUrl=" +
- encodeURIComponent(captureFilePath)
- );
- logger({
- cnl: ["server"],
- act: "同步比对api成功",
- ext: {
- isPass: res.data.isPass,
- isStranger: res.data.isStranger,
- errorMsg: res.data.errorMsg,
- },
- });
- emit("on-recognize-result", {
- isPassed: res.data.isPass,
- isStranger: res.data.isStranger,
- });
- } catch (e) {
- logger({
- cnl: ["server"],
- act: "同步比对失败",
- possibleError: e,
- });
- throw new Error("同步照片比较失败!");
- }
- }
- //#endregion 同步人脸比对
- //#region 异步人脸比对
- async function snapAsync() {
- try {
- logger({ cnl: ["server"], act: "定时抓拍开始" });
- const captureBlob = await getSnapShot(false);
- if (!(captureBlob instanceof Blob)) return;
- logger({ cnl: ["server"], act: "抓拍照片的大小:" + captureBlob.size });
- if (captureBlob.size < 48 * 48 || captureBlob.size >= 2 * 1024 * 1024) {
- // 经查以前记录,不完整图片均为8192大小。此处设置小于10KB的图片为未抓拍成功
- // 检查百度统计的记录后,这里的图片大小可能小于8192,也可能是有效的数据,所以降低图片大小的要求为face++的要求
- logger({
- cnl: ["server"],
- act: "摄像头异常",
- dtl: "定时抓拍照片大小异常",
- ext: { blobSize: captureBlob.size },
- });
- throw new Error("定时抓拍照片大小异常");
- }
- const startTime = Date.now();
- const [captureFilePath, signIdentifier] = await uploadToServer(captureBlob);
- const endTime = Date.now();
- logger({
- cnl: ["server"],
- act: "定时抓拍上传",
- ext: { cost: endTime - startTime },
- });
- await faceCompare(captureFilePath, signIdentifier, examRecordDataId);
- logger({
- cnl: ["server"],
- act: "定时抓拍比对",
- dtl: "定时抓拍流程成功",
- ext: { cost: Date.now() - endTime, signIdentifier },
- });
- } catch (error) {
- if (!(error instanceof Error)) {
- logger({
- cnl: ["server"],
- act: "snapAsync",
- dtl: "not an Error",
- stk: error + "",
- });
- return;
- }
- logger({
- cnl: ["server"],
- act: "定时抓拍流程失败",
- ejn: JSON.stringify(error),
- stk: error.stack,
- possibleError: error,
- });
- emit("on-async-recognize-result", {
- hasError: true,
- fileName: "",
- });
- }
- }
- type CameraInfo = {
- detail: string;
- pid: string;
- vid: string;
- name: string;
- };
- async function faceCompare(
- captureFilePath: string,
- signIdentifier: string,
- examRecordDataId: number
- ) {
- try {
- let cameraInfos;
- let hasVirtualCamera = false;
- if (typeof window.nodeRequire != "undefined") {
- const fs: typeof import("fs") = window.nodeRequire("fs");
- if (fileExists("multiCamera.exe")) {
- try {
- await execLocal("multiCamera.exe");
- cameraInfos = fs.readFileSync("CameraInfo.txt", "utf-8");
- // cameraInfos =
- // '[{"detail":"@device:pnp:?display#int3470#4&300121c4&0&uid13424#{65e8773d-8f56-11d0-a3b9-00a0c9223196}{9c5f415a-02cd-4e28-aeb7-811cb317dd64}","name":"HP Truevision 5MP Front","pid":"13424","vid":"3470"},{"detail":"@device:pnp:?display#int3470#4&300121c4&0&uid13424#{65e8773d-8f56-11d0-a3b9-00a0c9223196}{a6c1c503-01f1-4767-a229-00a0b223162f}","name":"HP Truevision 8MP Rear","pid":"13424","vid":"3470"},{"detail":"@device:pnp:?usb#vid_8086&pid_0a80&mi_04#6&28913c47&0&0004#{65e8773d-8f56-11d0-a3b9-00a0c9223196}global","name":"Intel(R) RealSense(TM) 3D Camera (R200) RGB","pid":"0a80","vid":"8086"},{"detail":"@device:pnp:?usb#vid_8086&pid_0a80&mi_00#6&28913c47&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}global","name":"Intel(R) RealSense(TM) 3D Camera (R200) Left-Right","pid":"0a80","vid":"8086"},{"detail":"@device:pnp:?usb#vid_8086&pid_0a80&mi_02#6&28913c47&0&0002#{65e8773d-8f56-11d0-a3b9-00a0c9223196}global","name":"Intel(R) RealSense(TM) 3D Camera (R200) Depth","pid":"0a80","vid":"8086"}]';
- if (cameraInfos && cameraInfos.trim()) {
- cameraInfos = cameraInfos.trim();
- cameraInfos = cameraInfos.replace(/\r\n/g, "");
- cameraInfos = cameraInfos.replace(/\n/g, "");
- logger({
- cnl: ["server"],
- act: "multiCamera.exe",
- ext: { cameraInfos },
- });
- }
- if (cameraInfos.includes('""')) {
- hasVirtualCamera = true;
- }
- // multiCamera.exe 1.0.1
- if (cameraInfos.includes("cameraInfo")) {
- cameraInfos = JSON.stringify(JSON.parse(cameraInfos).cameraInfo);
- }
- if (cameraInfos.length >= 800) {
- logger({
- cnl: ["server"],
- act: "multiCamera.exe",
- stk: "虚拟摄像头-cameraInfos超长",
- ext: { cameraInfos },
- });
- let ary: CameraInfo[] = JSON.parse(cameraInfos);
- // 相同pid&vid仅保留一个
- const pidAndVidCollector: string[] = [];
- ary = ary.filter((c) => {
- const pv = c.pid + "|" + c.vid;
- const res = pidAndVidCollector.includes(pv);
- pidAndVidCollector.push(pv);
- return !res;
- });
- cameraInfos = JSON.stringify(ary);
- logger({
- cnl: ["server"],
- act: "multiCamera.exe",
- stk: "除重复pid&vid",
- });
- if (cameraInfos.length >= 800) {
- cameraInfos = JSON.stringify(
- (<CameraInfo[]>JSON.parse(cameraInfos)).map((v) => {
- return {
- pid: v.pid,
- vid: v.pid,
- detail: "omitted",
- name: v.name,
- };
- })
- );
- console.log("摄像头检测超长:", "去除detail");
- console.log(cameraInfos);
- }
- if (cameraInfos.length >= 800) {
- logger({
- cnl: ["server"],
- act: "multiCamera.exe",
- stk: "精简后还是超长",
- ext: { cameraInfos },
- });
- }
- }
- } catch (error) {
- logger({
- cnl: ["server"],
- act: "multiCamera.exe",
- stk: "虚拟摄像头-读取摄像头列表失败",
- possibleError: error,
- });
- // throw new Error("读取摄像头列表失败");
- }
- }
- }
- let body: any = {
- fileUrl: captureFilePath,
- signIdentifier,
- examRecordDataId,
- };
- if (cameraInfos) {
- body.cameraInfos = cameraInfos;
- body.hasVirtualCamera = hasVirtualCamera;
- }
- logger({
- cnl: ["server"],
- act: "抓拍照片详细日志",
- ext: {
- fileUrl: captureFilePath,
- signIdentifier,
- examRecordDataId,
- cameraInfos,
- hasVirtualCamera,
- },
- });
- const res = await httpApp.post(
- "/api/ecs_oe_student_face/examCaptureQueue/uploadExamCapture",
- body
- );
- emit("on-async-recognize-result", {
- hasError: false,
- fileName: res.data,
- });
- } catch (e) {
- logger({
- cnl: ["server"],
- act: "定时抓拍",
- dtl: "抓拍失败",
- possibleError: e,
- });
- emit("on-async-recognize-result", {
- hasError: true,
- fileName: "",
- });
- throw new Error("异步比较抓拍照片失败");
- }
- }
- //#endregion 异步人脸比对
- </script>
- <template>
- <div>
- <video id="video" ref="video" :width="width" :height="height" autoplay />
- <div v-if="showRecognizeButton" class="btn-container">
- <button
- class="verify-button"
- :class="[snapBtnDisabled && 'disable-verify-button']"
- :disabled="snapBtnDisabled"
- @click="snapSync"
- >
- {{ btnText }}
- </button>
- </div>
- </div>
- </template>
- <style scoped>
- .btn-container {
- position: absolute;
- width: 400px;
- text-align: center;
- margin-top: -50px;
- color: #232323;
- }
- .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>
|