需求:
1、前端实现模拟用户人脸识别,识别成功后抓取视频流或认证的一张静态图给服务端。
2、服务端调用第三方活体认证接口,验证前端传递的人脸是否存在,把认证结果反馈给前端。
3、前端根据服务端返回的状态,显示在页面上用于提示用户。
难点:
1、前端APP如果要实现人脸活体校验验证,需要对接大厂的SDK实现。
2、一开始我采用用使用在App内嵌套H5来单独部署一套人脸验证,把结果通过webview与APP进行数据交互,但是H5试了使用好用高效的effet.js 库人脸识别 目前仅支持H5(但是发现最后在手机上识别人像整个人被压缩似得,而且是反向镜头,跟作者已反馈,等待作者持续更新)。
3、抛弃了使用App嵌套H5方法,因此最终选择了使用原生live-pusher直播流来模拟实现人脸识别效果。本打算给大家写成组件方便大家直接调用来着,但是发现组件内获取实例仅支持在onReady页面生命周期使用。
实现思路
1、首先需要获取手机是否有录音以及相机权限,没有的话引导用户前去设置页主动设置。
2、其次创建live-pusher实例,根据业务需求实现自己的模拟人脸识别思路(目前我们这是在用户手动点击5s后进行自动抓拍的)。
3、前端拿到抓拍最后一帧图片调用接口给服务端传递,服务端调用第三方进行人脸活体检测,一般是需要付费的哈。
4、最后前端把服务端返回的识别状态展示在页面上,方便后续用户操作。
代码步骤(当前是vue3项目示例,最低sdk 21版本并且勾选livepusher)
1、获取当前手机是否允许开启相机和录音权限。(建议直接使用官方大佬写的js sdk)App权限判断和提示
引入js sdk插件(vue3版本需要转换为export function,vue2版本直接按照官方大佬的直接使用即可)
import {
requestAndroidPermission,
gotoAppPermissionSetting
} from '@/js_sdk/wa-permission/permission.js'
2、创建live-pusher实例,根据业务需求写业务逻辑(注意:一定要用nvue页面哈,cover-image覆盖一个矢量图在直播流画面上)。
ilve-pusher详细参数说明具体看live-pusher官方文档
<template>
<view class="container">
<live-pusher id='livePusher' ref="livePusher" class="livePusher" url="" mode="FHD" :muted="true"
:enable-camera="true" :auto-focus="true" :beauty="2" whiteness="2" aspect="9:16" local-mirror="disable"
@statechange="statechange" @error="error" @netstatus="netstatus" :style="[{
width:'400rpx',
height: '400rpx',
marginLeft: '175rpx',
marginTop:'20rpx',
}]"></live-pusher>
<cover-image style="
width: 400rpx;
height: 400rpx;
transform: scale(1.01);
position: absolute;
left: 175rpx;
top: 190.5rpx;
" src="@/static/circleBg.png" />
</view>
</template>
<script>
export default{
onReady() {//需要在onReady页面生命周期函数来写
this.livePusher = uni.createLivePusherContext("livePusher", this);
},
}
</script>
3、5s后抓拍最后一帧图片给服务端,人脸识别成功即登录系统,识别失败跳转认证失败页面(这里跟服务端对接采用的formdata格式上传文件流,你也可以采取转成base64)
临时路径转base64
// 定时器 几秒后自动抓拍
handleSetTime() {
this.timeFlag = setInterval(async () => {
if (this.timeOut > 0) {
this.timeOut--
this.titleTips = this.countDownTimerStartTips
this.buttonTip = `${this.countDownTimerStartBtnTips} ${this.timeOut}秒`
}
if (this.timeOut == 1) {
this.livePusher.snapshot({
success: (res) => {
this.snapshotInfo = res.message
}
})
}
// 进行快照人脸认证
if (this.timeOut == 0) {
clearInterval(this.timeFlag);
this.titleTips = this.countDownTimerZeroTips
this.buttonTip = this.countDownTimerZeroBtnTips
uni.showLoading({
title: this.countDownTimerZeroBtnTips
})
uni.uploadFile({
url: 'http://192.168.60.2:8080/bsCheckImage/checkImg',
filePath: this.snapshotInfo.tempImagePath,
name: "file",
success: (uploadFileRes) => {
const jxStrData = JSON.parse(uploadFileRes.data)
console.log(jxStrData)
const resResultCode = jxStrData.code
const resResultData = jxStrData.data
if (resResultCode !== '00000') {
uni.navigateTo({
url: '/pages/liveb-result/liveb-result?failResultObj=' +
this.passData(jxStrData)
})
this.handleStop()
return
}
if (resResultCode == '00000' && resResultData.score >= 0.8) {
uni.showToast({
title: this.faceSucessTips
})
this.buttonTip = this.faceSucessTips
this.handleStop()
return
}
if (resResultCode == '00000' && resResultData.score < 0.8) {
const paramsData = {
success: false,
code: 'A9901',
message: '人脸校验失败,请将人脸正对取景框内重新认证',
failCode: 'A9901',
faceScore: resResultData.score
}
uni.navigateTo({
url: '/pages/liveb-result/liveb-result?failResultObj=' +
this.passData(paramsData)
})
this.handleStop()
return
}
},
fail: (error) => {
uni.hideLoading()
uni.navigateTo({
url: '/pages/liveb-result/liveb-result',
animationType: 'zoom-out',
animationDuration: 1000
})
this.handleStop()
},
complete: () => {
uni.hideLoading()
clearInterval(this.timeFlag) // 清除定时器,防止再次执行
}
});
}
}, 1000)
},
4、人脸认证失败服务端返回状态,前端跳转认证失败页面,返回时给上个页面传递监听参数。(liveb-result.vue页面)
<template>
<view class="container">
<view class="result-area">
<view class="result-icon">
<image class="result-icon-pic" src="../../static/fece_result.png"></image>
</view>
<view class="result-tips">{{failInfos.message}}</view>
<view class="result-button" @click="handleRetryFace">重新认证</view>
</view>
</view>
</template>
<script setup>
import {
ref
} from 'vue'
import {
onLoad,
onBackPress
} from '@dcloudio/uni-app'
const faceStatus = ref('')
const failInfos = ref({})
const failResultMsg = (() => {
const data = {
code: '3698',
msg: '人脸认证失败'
}
uni.$emit('failResult', data);
})
onLoad((options) => {
if (options.failResultObj) {
const resultObj = JSON.parse(decodeURIComponent(options.failResultObj));
failInfos.value = resultObj
}
})
const handleRetryFace = (() => {
console.log('handleRetryFace')
failResultMsg()
uni.navigateBack()
})
onBackPress((e) => {
failResultMsg()
})
</script>
<style lang="scss" scoped>
.container {
width: 750rpx;
.result-area {
position: absolute;
top: 44%;
left: 50%;
transform: translate(-50%, -50%);
.result-icon {
display: flex;
align-items: center;
justify-content: center;
.result-icon-pic {
width: 140rpx;
height: 140rpx;
}
}
.result-tips {
font-weight: 600;
text-align: center;
font-size: 32rpx;
color: #515151;
margin-top: 20rpx;
margin-bottom: 60rpx;
}
.result-button {
padding: 20rpx 100rpx;
background-color: rgba(12, 75, 158, 1);
border-radius: 60rpx;
color: #eeeeee;
}
}
}
</style>
人脸识别页面所有代码(liveb.nvue)
<template>
<view class="container">
<view class="header">
<view class="header-title">
<text class="header-title-tips">{{titleTips}}</text>
<view class="header-title-carmera">
<image class="header-title-img" src="../../static/change_camera.png" @click="handleChangeCrame">
</image>
</view>
</view>
</view>
<live-pusher id='livePusher' ref="livePusher" class="livePusher" url="" mode="FHD" :muted="true"
:enable-camera="true" :auto-focus="true" :beauty="2" whiteness="2" aspect="9:16" local-mirror="disable"
@statechange="statechange" @error="error" @netstatus="netstatus" :style="[{
width:'400rpx',
height: '400rpx',
marginLeft: '175rpx',
marginTop:'20rpx',
}]"></live-pusher>
<cover-image style="
width: 400rpx;
height: 400rpx;
transform: scale(1.01);
position: absolute;
left: 175rpx;
top: 190.5rpx;
" src="@/static/circleBg.png" />
<view class="footer">
<view class="footer-tips">
<text class="footer-tips-first">
{{footerTipsFirst}}
</text>
<text class="footer-tips-second">
{{footerTipsSecond}}
</text>
</view>
<view class="footer-required">
<view class="footer-required-row">
<view class="row-area" v-for="(item,index) in tipList" :key="index">
<image class="row-area-img" :src="item.icon">
</image>
<text class="row-area-tip">
{{item.name}}
</text>
</view>
</view>
</view>
</view>
<!-- 手动抓拍 -->
<view class="start-button" :style="{marginTop:footerBtnStyle.marginTop}">
<view class="button-hover"
:style="{width:footerBtnStyle.width,padding:footerBtnStyle.padding,borderRadius:footerBtnStyle.borderRadius,backgroundColor:footerBtnStyle.btnBackground}"
@click="startFace">
<text class="button-tip" :style="{
fontSize:footerBtnStyle.fontSize,color:footerBtnStyle.textColor
}">{{buttonTip}}</text>
</view>
</view>
</view>
</template>
<script>
import {
requestAndroidPermission,
gotoAppPermissionSetting
} from '@/js_sdk/wa-permission/permission.js'
export default {
name: 'sevenq-faceLiver',
props: {
//是否默认开启抓拍
isDeaultStartLive: {
type: Boolean,
default: false
},
//默认开启的话需要设置延迟时间(毫秒级)
defaultStartDelayTime: {
type: Number,
default: 600
},
//是否需要监听结果页传递的事件
needListenResultPage: {
type: Boolean,
default: true
},
//是否开启可以翻转摄像头
isCanChangeCarame: {
type: Boolean,
default: true
},
//抓拍倒计时 (如果默认开启需要+1)
snapCountdownTimer: {
type: Number,
default: 6
},
//如果不允许翻转摄像头 提示词
notAllowChangeCarameMsg: {
type: String,
default: "刷脸认证仅支持前置摄像头"
},
//顶部提示词
topTitleTips: {
type: String,
default: "请把人脸放在圆圈内拍摄脸部,开始人脸识别"
},
//提示词 1
footerTipsFirst: {
type: String,
default: "确认为您本人照片"
},
//提示词 2
footerTipsSecond: {
type: String,
default: "保持正脸在取景框中系统将在5s后自动抓拍"
},
//提示展示列表
tipList: {
type: Object,
default: [{
icon: "../../static/img3.png",
name: '正对手机'
},
{
icon: "../../static/img2.png",
name: '光线充足'
},
{
icon: "../../static/img1.png",
name: '脸无遮挡'
},
]
},
//抓拍倒计时开始时提示词
countDownTimerStartTips: {
type: String,
default: "请保存人脸在实景框中,正在进行抓拍"
},
//抓拍倒计时为0时提示词
countDownTimerZeroTips: {
type: String,
default: "正在人脸认证中,请稍等..."
},
//按钮默认文本
buttonTips: {
type: String,
default: "开始人脸识别"
},
//抓拍倒计时开始时按钮显示提示词
countDownTimerStartBtnTips: {
type: String,
default: "正在抓拍中"
},
//抓拍倒计时为0时按钮提示词
countDownTimerZeroBtnTips: {
type: String,
default: "人脸认证中...."
},
//认证成功按钮提示词
faceSucessTips: {
type: String,
default: "认证成功"
},
//权限提示开启提示词
premissonTips: {
type: String,
default: "当前应用需要使用相机权限进行拍照,但相机权限暂未开启。是否前往应用设置打开相机权限?"
},
//底部按钮样式
footerBtnStyle: {
type: Object,
default: {
marginTop: '120rpx',
width: '480rpx',
padding: '24rpx',
btnBackground: 'rgba(12, 75, 158, 1)',
borderRadius: '300rpx',
textColor: '#dfdfdf',
fontSize: '32rpx'
}
}
},
data() {
return {
titleTips: this.topTitleTips,
buttonTip: this.buttonTips,
livePusher: '', // livePusher实例
snapshotInfo: '', // 快照信息
showCountDown: false, // 拍摄倒计时
timeOut: this.snapCountdownTimer, // 签到倒计时
timeFlag: null, // 定时器
isPass: null, // 是否通过人脸认证
phoneSysInfos: {}, //当前手机系统信息
}
},
onReady() {
this.livePusher = uni.createLivePusherContext("livePusher", this);
},
onShow() {
//监听结果页面传递的失败事件
if (this.needListenResultPage) {
uni.$on('failResult', (resultData) => {
if (resultData.code == '3698') {
clearInterval(this.timeFlag)
this.resertAll()
this.livePusher.startPreview()
}
});
}
},
async mounted() {
const that_ = this
if (!that_.showCountDown) {
setTimeout(function() {
that_.getCarmeraPremisson()
}, this.defaultStartDelayTime)
}
that_.getPhoneSys()
},
onUnload() {
if (this.needListenResultPage) {
uni.$off('failResult');
}
clearInterval(this.timeFlag)
uni.hideLoading();
},
onHide() {
console.log('页面隐藏')
},
methods: {
//校验是否获取相机权限
async getCarmeraPremisson() {
const currentSystem = uni.getSystemInfoSync().platform
if (currentSystem == 'android') {
const result = await requestAndroidPermission("android.permission.CAMERA")
if (result == 1) {
if (this.isDeaultStartLive) { //如果打开页面就进行抓拍
this.showCountDown = true
}
this.startPreview()
} else {
uni.showModal({
title: '提示',
content: this.premissonTips,
confirmText: '去设置',
cancelText: '取消',
success: function(res) {
if (res.confirm) {
gotoAppPermissionSetting()
} else if (res.cancel) {
uni.showToast({
icon: 'error',
title: '暂无相机权限'
})
}
}
});
}
}
},
//重置初始化值 需要在认证失败时候再次调用
resertAll() {
this.titleTips = this.topTitleTips
this.buttonTip = this.buttonTips
this.snapshotInfo = '' // 快照信息
this.showCountDown = false // 拍摄倒计时
this.timeOut = this.snapCountdownTimer // 签到倒计时
this.timeFlag = null // 定时器
this.isPass = null // 是否通过人脸认证
},
//手动翻转摄像头
handleChangeCrame() {
if (!this.isCanChangeCarame) {
uni.showToast({
icon: 'none',
title: this.notAllowChangeCarameMsg
})
return
}
this.livePusher.switchCamera({
success: (a) => {
uni.showToast({
icon: 'none',
title: '摄像头翻转成功'
})
}
});
},
//手动开始人脸识别
startFace() {
const that_ = this
if (!that_.showCountDown) {
that_.showCountDown = true
if (that_.showCountDown) {
const {
platform,
osVersion
} = that_.phoneSysInfos
if (platform == 'android' && osVersion < 10) { //判断兼容安卓10以下效果
that_.startPreview()
return
}
that_.handleSetTime()
}
}
},
// 开始预览直播流
startPreview() {
const _that = this
this.livePusher.startPreview({
success: (res) => {
if (_that.showCountDown) {
_that.handleSetTime()
}
}
})
},
// 定时器 几秒后自动抓拍
handleSetTime() {
this.timeFlag = setInterval(async () => {
if (this.timeOut > 0) {
this.timeOut--
this.titleTips = this.countDownTimerStartTips
this.buttonTip = `${this.countDownTimerStartBtnTips} ${this.timeOut}秒`
}
if (this.timeOut == 1) {
this.livePusher.snapshot({
success: (res) => {
this.snapshotInfo = res.message
}
})
}
// 进行快照人脸认证
if (this.timeOut == 0) {
clearInterval(this.timeFlag);
this.titleTips = this.countDownTimerZeroTips
this.buttonTip = this.countDownTimerZeroBtnTips
uni.showLoading({
title: this.countDownTimerZeroBtnTips
})
// this.$emit(handleStartFaceApi, {
// code: '4364',
// msg: '开始人脸与服务端进行人脸',
// currentTempImagePath: this.snapshotInfo.tempImagePath
// })
uni.uploadFile({
url: 'http://192.168.60.2:8080/bsCheckImage/checkImg',
filePath: this.snapshotInfo.tempImagePath,
name: "file",
success: (uploadFileRes) => {
const jxStrData = JSON.parse(uploadFileRes.data)
console.log(jxStrData)
const resResultCode = jxStrData.code
const resResultData = jxStrData.data
if (resResultCode !== '00000') {
uni.navigateTo({
url: '/pages/liveb-result/liveb-result?failResultObj=' +
this.passData(jxStrData)
})
this.handleStop()
return
}
if (resResultCode == '00000' && resResultData.score >= 0.8) {
uni.showToast({
title: this.faceSucessTips
})
this.buttonTip = this.faceSucessTips
this.handleStop()
return
}
if (resResultCode == '00000' && resResultData.score < 0.8) {
const paramsData = {
success: false,
code: 'A9901',
message: '人脸校验失败,请将人脸正对取景框内重新认证',
failCode: 'A9901',
faceScore: resResultData.score
}
uni.navigateTo({
url: '/pages/liveb-result/liveb-result?failResultObj=' +
this.passData(paramsData)
})
this.handleStop()
return
}
},
fail: (error) => {
uni.hideLoading()
uni.navigateTo({
url: '/pages/liveb-result/liveb-result',
animationType: 'zoom-out',
animationDuration: 1000
})
this.handleStop()
},
complete: () => {
uni.hideLoading()
clearInterval(this.timeFlag) // 清除定时器,防止再次执行
}
});
}
}, 1000)
},
//向下个页面传递参数
passData(obj) {
let passDataStr = JSON.stringify(obj)
let newPassDataStr = passDataStr.replace(/%/g, '%25');
return encodeURIComponent(newPassDataStr);
},
//抛出停止推流 在调用成功与失败都得调用
handleStop() {
this.livePusher.stop()
},
//监听直播流状态变化
statechange(val) {
console.log(val, '监听直播流变化')
},
//监听直播流警告
error(err) {
console.log(err, '监听直播流警告')
},
//监听网络状态
netstatus(status) {
console.log(status, '监听直播流网络状态')
},
//获取手机型号
getPhoneSys() {
const system = uni.getDeviceInfo()
this.phoneSysInfos = system
}
}
}
</script>
<style lang="scss" scoped>
.container {
width: 750rpx;
}
</style>