前端实现人脸识别(活体检测)

1,643 阅读2分钟

🥑 抱怨黑暗深处,不如提灯前行

image.png 其实也就是传一个视频给后端,后端去调用第三方检测

一、啥也不说,先绘制页面

采用flex布局

image.png

<template>
  <kyc-header :title="'06 活体验证'" />
  <div class="content" v-show="show">
    <div class="number">
      <div :key="index" class="alone" v-for="(item,index) in numList">{{item}}</div>
    </div>
    <div class="reflash">
      <p class="left">请正确念出上方数字</p>
      <p @click="claim" class="right">刷新数字</p>
    </div>
    <div class="face">
      <img alt src="@/assets/img/living_auth.png" v-if="!showVideo" />
      <!-- 人脸识别 -->
      <div class="video" v-show="showVideo">
        <video
          :src="url"
          autoplay
          muted
          playsinline
          ref="videoRef"
          webkit-playsinline="true"
          x5-video-player-type="h5"
        ></video>
      </div>
    </div>
    <div class="btn-footer">
      <van-button @click.native="getCamera" class="btn" color="#00A4B7" v-if="btnWord==1">开始录制</van-button>
      <van-button @click="saveVideo" class="btn" color="#e96040" v-if="btnWord==2">结束录制</van-button>
      <van-button @click="reVideo" class="btn" color="#eff0f2" v-if="btnWord==3">重新录制</van-button>
    </div>
    <div class="explain">
      <p>视频录制说明</p>
      <ul>
        <li>1.请使用前置摄像头</li>
        <li>2.保证光线充足、脸部完全入镜、脸部无遮挡物</li>
        <li>3.录制视频过程中请使用普通话读一遍上方验证数字</li>
        <li>4.如验证失败、数字失效,点击“刷新数字”后重新录制</li>
        <li>5.如录制不满意可点击“重新录制”直至录制满意</li>
      </ul>
    </div>
  </div>
  <van-sticky class="page_bottom" position="bottom">
    <van-button
      :class="{disable_bgc:true,click_bgc:btnWord==3}"
      :disabled="btnWord!==3"
      @click="handleSubmit"
      block
      class="btn-box"
    >提交</van-button>
  </van-sticky>
</template>
<style lang='scss' scoped>
  .content {
    position: absolute;
    top: 50px;
    bottom: calc(50px + constant(safe-area-inset-bottom));
    bottom: calc(50px + env(safe-area-inset-bottom));
    overflow: auto;
    width: 100%;
    -webkit-overflow-scrolling: touch;
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    align-items: center;
    :deep(.van-button) {
      width: 200px;
    }
  }
  .number {
    width: 80%;
    display: flex;
    justify-content: space-between;
    flex-wrap: nowrap;
    .alone {
      width: 55px;
      height: 55px;
      font-size: 32px;
      text-align: center;
      background-color: #fff;
    }
  }
  p {
    font-size: 14px;
    color: #999999;
    margin-bottom: 0;
  }
  .reflash {
    height: 20px;
    text-align: center;
    justify-content: center;
    .left {
      display: block;
      float: left;
    }
    .right {
      display: block;
      float: right;
      color: #00a4b7;
    }
  }
  .face {
    border-radius: 50%;
    width: 200px;
    height: 200px;
    border: 1px solid #ffffff;
    img {
      width: 100%;
      height: 100%;
    }
  }

  .explain {
    p {
      margin-bottom: 15px;
    }
    ul {
      li {
        font-size: 12px;
        color: #999999;
        margin-bottom: 4px;
      }
    }
  }
  //录制
  .video {
    border-radius: 50%;
    width: 100%;
    height: 100%;
    video {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      object-fit: cover;
      background-repeat: no-repeat;
      background-size: 100% 100%;
      z-index: 0;
    }
  }
</style>

二、接口

image.png

三、调用浏览器摄像头和麦克风权限方法

  • 其中 constraints 为需要获取的权限列表,这里只需要指定音频 audio 即可
  • 其返回是个 Promise,因为用户何时进行授权是不确定的。通过在 Promise 的回调中进行授权成功或失败的处理。
  • 在使用前需要判断浏览器是否已经支持相应的 API,此时得到如下的代码:
const getCamera = () => {
// 0️⃣ constraints 为需要获取的权限列表,这里只需要指定音频和视频
        let constraints = {
          audio: true,  //🎤
          video: {
            facingMode: 'user', // 优先调前置摄像头
          },
        }
        // 1️⃣ 在使用前需要判断浏览器是否已经支持相应的 API
        // 老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象
        if (navigator.mediaDevices === undefined) {
          navigator.mediaDevices = {}
        }
        // 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia,因为这样可能会覆盖已有的属性。这里我们只会在没有getUserMedia属性的时候添加它。
        if (navigator.mediaDevices.getUserMedia === undefined) {
          navigator.mediaDevices.getUserMedia = function (constraints) {
            // 判断浏览器是否支持getUserMedia方法
            var getUserMedia =
              navigator.getUserMedia ||
              navigator.webkitGetUserMedia ||
              navigator.mozGetUserMedia
            if (!getUserMedia) {
              Toast('该浏览器不支持getUserMedia,请使用其他浏览器')
              return Promise.reject(
                new Error('getUserMedia is not implemented in this browser')
              )
            }
            // 否则,为老的navigator.getUserMedia方法包裹一个Promise
            return new Promise(function (resolve, reject) {
              getUserMedia.call(navigator, constraints, resolve, reject)
            })
          }
        }
        // 3️⃣ 使用浏览器提供的 MediaRecorder API
        navigator.mediaDevices
          .getUserMedia(constraints)
          .then((stream) => {
          // 4️⃣ 其中成功回调里得到的入参 stream 为 MediaStream 对象。
            state.MediaStreamTrack =
              typeof stream.stop === 'function' ? stream : stream.getTracks()[0]
            state.showVideo = true // 显示录制框
            state.isAlreadyRecord = false
            let winURL = window.URL || window.webkitURL
            if ('srcObject' in proxy.$refs.videoRef) {
              proxy.$refs.videoRef.srcObject = stream
            } else {
              proxy.$refs.videoRef.src = winURL.createObjectURL(stream)
            }
            proxy.$refs.videoRef.onloadedmetadata = (e) => {
              if (stream.active) {
                proxy.$refs.videoRef.play()
                state.btnWord = 2
              }
            }
            let options = {
              videoBitsPerSecond: 2500000,
            }
            state.mediaRecorder = new MediaRecorder(stream, options)
            saveVideo(stream) //5️⃣ 摄像头和🎤权限获取完毕,开始录制视频
          })
          .catch((err) => {
            // console.log(err)
            Toast('摄像头开启失败,请检查摄像头是否授权或是否可用!')
          })
      }

保存录制视频(是要传给后端滴)

const saveVideo = (stream) => {
        if (state.isAlreadyRecord) {  // 7️⃣ 结束录制
          //当录制的数据可用时
          state.mediaRecorder.ondataavailable = (e) => {
            if (e.data && e.data.size > 0) {
              state.recordedBlobs.push(e.data)
            }
          }
          state.mediaRecorder.stop() //8️⃣ 调用浏览器api关闭摄像头和🎤
          state.isAlreadyRecord = false
          const stream = proxy.$refs.videoRef.srcObject
          const tracks = stream.getTracks()
          tracks.forEach(function (track) {
            track.stop()
          })
          proxy.$refs.videoRef.srcObject = null
          state.showVideo = false
          state.btnWord = 3 // 9️⃣ 按钮变成"重新录制"
        } else { // 6️⃣ 开始录制
          state.isAlreadyRecord = true
          state.mediaRecorder.start()
        }
      }

新定义一个变量isAlreadyRecord用来区分 开始录制 or 结束录制

  1. 初始化为falseisAlreadyRecord: false,
  2. 柚子点击“开始录制”按钮state.isAlreadyRecord = true
  3. 点击“结束录制”按钮,state.isAlreadyRecord = false

重新录制功能[reVideo()]

先清除上一次录制的视频二进制流[closeVideo()],再次调用getCamera()

//重新录制
      const reVideo = () => {
        state.btnWord = 2
        closeVideo()
        getCamera()
      }
const closeVideo = () => {
        state.recordedBlobs = []
        // state.isAlreadyRecord = false
        // state.MediaStreamTrack && state.MediaStreamTrack.stop()
      }
 

base64转为文件流

 //base64转为文件流
      const base64toFile = (dataurl, filename = 'file') => {
        let arr = dataurl.split(',')
        let mime = arr[0].match(/:(.*?);/)[1]
        let suffix = mime.split('/')[1]
        let bstr = atob(arr[1])
        let n = bstr.length
        let u8arr = new Uint8Array(n)
        while (n--) {
          u8arr[n] = bstr.charCodeAt(n)
        }
        return new File([u8arr], `${filename}.${suffix}`, {
          type: mime,
        })
      }

提交按钮click事件

const handleSubmit = () => {
        var blob = new Blob(state.recordedBlobs, { type: 'video/mp4' })
        var reader = new FileReader()
        reader.readAsDataURL(blob, 'utf-8')
        reader.onload = () => {
          let str = base64toFile(reader.result) //reader.result为base64格式的视频
          let formData = new FormData()
          formData.append('file', str) //此时str是文件流
          uploadVideo(formData)
        }
      }

上传视频接口调用

 //上传视频接口
      const uploadVideo = (formData) => {
        _living
          .infoSubmit(formData)
          .then((res) => {
            proxy.$router.push('/living')
          })
          .catch((err) => {
            claim()
          })
      }

四、附代码

[页面全部代码](www.yuque.com/docs/share/… 《27 人脸识别代码》)

五、vue.config.js添加https:true

devServer: {
    https: true,
    port: '9091',
  },