写在前面:
注意:小程序官方不支持采集人脸等用户隐私信息,采集用户信息建议使用官方提供的人脸核身接口,其他任何第三方的都审核不过。人脸核身需要一些资质(部分类目的小程序)。
参考官方说明:
1.微信人脸核身接口能力说明
2. 腾讯人脸核身SDK接入
本案例是另外一种实现方式,使用了组件和wx.faceDetect小程序API,只是作代码演示,并不建议在生产中使用,因为可能过不了审核。
录制思路:
使用小程序的
组件 和 CameraContext.startRecord等接口开启摄像头录制。 使用wx.faceDetect()人脸识别接口对摄像头的视频流帧进行识别(检测是否是人脸且是正脸)。
需要注意的是:
- 用户是否授权摄像头和录音。
- 用户的微信版本是否可以调用wx.faceDetect接口(基础库:2.18.0)。
- 用户人脸移出/不是正脸取消录制,并在正脸时重新录制。
- 准备录制-录制中-录制完成几种状态文案切换,还有一句录制中倒计时提示。
- 视频帧检测调用函数节流,防止调用wx.faceDetect过于频繁引起卡顿(影响识别到人脸的时间)。
- 开发者工具开启增强编译,修改成你的appid,且需要真机预览,调试和在pc模拟器中会报错。
截图案例 (非真实截图):
录像组件核心代码:
准备录制人脸
{{bottomTips}}
// components/camera-face/index.js
import { getAuthorize, setAuthorize, throttle, checkVersion } from './utils'
// 提示信息
const tips = {
ready: '请确保光线充足,正面镜头',
recording: '人脸录制中..',
complete: '已录制完成',
error: '录制失败'
}
Component({
// 组件的属性列表
properties: {
// 人脸整体可信度 [0-1], 参考wx.faceDetect文档的res.confArray.global
// 当超过这个可信度且正脸时开始录制人脸, 反之停止录制
faceCredibility: {
type: Number,
value: 0.5
},
// 人脸偏移角度正脸数值参考wx.faceDetect文档的res.angleArray
// 越接近0越正脸,包括p仰俯角(pitch点头), y偏航角(yaw摇头), r翻滚角(roll左右倾)
faceAngle: {
type: Object,
value: { p: 0.5, y: 0.5, r: 0.5 }
},
// 录制视频时长,不能超过30s
duration: {
type: Number,
value: 3000
},
// 是否压缩视频
compressed: {
type: Boolean,
value: false
},
// 前置或者后置 front,back
devicePosition: {
type: String,
value: 'front'
},
// 指定期望的相机帧数据尺寸 small,medium,large
frameSize: {
type: String,
value: 'medium'
},
// 分辨率 low,medium,high
resolution: {
type: String,
value: 'medium'
},
// 闪光灯 auto,on,off,torch
flash: {
type: String,
value: 'off'
},
// 检测视频帧的节流时间,默认500毫秒执行一次
throttleFrequency: {
type: Number,
value: 500
}
},
// 组件页面的生命周期
pageLifetimes: {
// 页面被隐藏
hide: function() {
this.stop()
},
},
detached: function() {
// 在组件实例被从页面节点树移除时执行
this.stop()
},
// 组件的初始数据
data: {
isReading: false, // 是否在准备中
isRecoding: false, // 是否正在录制中
isStopRecoding: false, // 是否正在停止录制中
bottomTips: '', // 底部提示文字
},
/**
* 组件的方法列表
*/
methods: {
// 开启相机ctx
async start() {
const result = await this.initAuthorize();
if (!result) return false;
if (!this.ctx) this.ctx = wx.createCameraContext();
return true;
},
// 准备录制
async readyRecord() {
if (this.data.isReading) return
this.setData({ isReading: true })
wx.showLoading({ title: '加载中..', mask: true })
// 检测版本号
const canUse = checkVersion('2.18.0', () => {
this.triggerEvent('cannotUse')
})
if (!canUse) {
wx.hideLoading()
this.setData({ isReading: false })
return
}
// 启用相机
try {
const result = await this.start()
if (!result || !this.ctx) throw new Error()
} catch (e) {
wx.hideLoading()
this.setData({ isReading: false })
return
}
console.log('准备录制')
this.setData({ bottomTips: tips.ready })
// 视频帧回调节流函数
let fn = throttle((frame) => {
// 人脸识别
wx.faceDetect({
frameBuffer: frame.data,
width: frame.width,
height: frame.height,
enableConf: true,
enableAngle: true,
success: (res) => this.processFaceData(res),
fail: (err) => this.cancel()
})
}, this.properties.throttleFrequency);
// 初始化人脸识别
wx.initFaceDetect({
success: () => {
const listener = this.listener = this.ctx.onCameraFrame((frame) => fn(frame));
listener.start();
},
fail: (err) => {
console.log('初始人脸识别失败', err)
this.setData({ bottomTips: '' })
wx.showToast({ title: '初始人脸识别失败', icon: 'none' })
},
complete: () => {
wx.hideLoading()
this.setData({ isReading: false })
}
})
},
// 处理人脸识别数据
processFaceData(res) {
if(res.confArray && res.angleArray) {
const { global } = res.confArray;
const g = this.properties.faceCredibility;
const { pitch, yaw, roll } = res.angleArray;
const { p, y, r } = this.properties.faceAngle;
console.log('res.confArray.global:', global)
console.log('res.angleArray:', pitch, yaw, roll)
const isGlobal = global >= g;
const isPitch = Math.abs(pitch) <= p;
const isYaw = Math.abs(yaw) <= y;
const isRoll = Math.abs(roll) <= r;
if( isGlobal && isPitch && isYaw && isRoll ){
console.log('人脸可信,且是正脸');
if (this.data.isRecoding || this.data.isCompleteRecoding) return
this.setData({ isRecoding: true });
this.startRecord(); // 开始录制
}else {
console.log('人脸不可信,或者不是正脸');
this.cancel()
}
}else {
console.log('获取人脸识别数据失败', res);
this.cancel()
}
},
// 开始录制
startRecord() {
console.log('开始录制')
this.ctx.startRecord({
success: (res) => {
this.setRecordingTips();
this.timer = setTimeout(() => {
this.completeRecord()
}, this.properties.duration)
},
timeoutCallback: (res) => {
// 超过30s或页面 onHide 时会结束录像
this.stop();
},
fail: () => this.stop()
})
},
// 设置录制中的提示文字和倒计时
setRecordingTips() {
let second = (this.properties.duration / 1000);
if (this.interval) clearInterval(this.interval);
this.interval = setInterval(() => {
console.log('xxxxxx', second);
this.setData({
bottomTips: tips.recording + second-- + 's'
})
if (second <= 0) clearInterval(this.interval);
}, 1000)
},
// 完成录制
completeRecord() {
console.log('完成录制');
this.setData({ isCompleteRecoding: true })
this.ctx.stopRecord({
compressed: this.properties.compressed,
success: (res) => {
this.setData({ bottomTips: tips.complete })
// 向外触发完成录制的事件
this.triggerEvent('complete', res.tempVideoPath)
},
fail: () => this.stop(),
complete: () => {
this.listener.stop();
wx.stopFaceDetect();
clearInterval(this.interval);
this.setData({ isCompleteRecoding: false })
}
})
},
// 人脸移出等取消录制
cancel() {
console.log('取消录制');
// 如果不在录制中或者正在录制完成中就不能取消
if (!this.data.isRecoding || this.data.isCompleteRecoding) return
clearTimeout(this.timer);
clearInterval(this.interval);
this.ctx.stopRecord({
complete: () => {
console.log('取消录制成功');
this.setData({ bottomTips: tips.ready, isRecoding: false });
}
});
},
// 用户切入后台等停止使用摄像头
stop() {
console.log('停止录制');
clearTimeout(this.timer);
clearInterval(this.interval);
if(this.listener) this.listener.stop();
if (this.ctx && !this.data.isCompleteRecoding) this.ctx.stopRecord()
wx.stopFaceDetect();
setTimeout(() => {
this.setData({ bottomTips: '', isRecoding: false })
}, 500)
},
// 用户不允许使用摄像头
error(e) {
// const cameraName = 'scope.camera';
// this.triggerEvent('noAuth', cameraName)
},
// 初始相机和录音权限
async initAuthorize() {
const cameraName = 'scope.camera';
const recordName = 'scope.record';
const scopeCamera = await getAuthorize(cameraName);
// 未授权相机
if (!scopeCamera) {
// 用户拒绝授权相机
if (!(await setAuthorize(cameraName))) this.openSetting();
return false;
}
const scopeRecord = await getAuthorize(recordName);
if (!scopeRecord) {
// 用户拒绝授权录音
if (!(await setAuthorize(recordName))) {
this.openSetting();
return false;
}
}
return true;
},
// 打开设置授权
openSetting() {
wx.showModal({
title: '开启摄像头和录音权限',
showCancel: true,
content: '是否打开?',
success: (res) => {
this.triggerEvent('noAuth', '打开设置授权')
if (res.confirm) {
wx.openSetting();
}
}
});
}
}
})
页面使用核心代码:
预览视频
// pages/page2/index.js
Page({
onHide() {
// 在录制中退出后台页面隐藏,返回上一页,确保重新进入当前页
// 防止在录制中退出后台导致下次重新录制失败 "operateCamera:fail:is stopping"
console.log('页面隐藏')
if (this.data.isBack) wx.navigateBack()
},
onShow() {
console.log('页面显示')
this.setData({ isBack: true })
},
data: {
videoSrc: '', // 录制的视频临时路径
isBack: false // 是否返回上一页,用于页面隐藏时判断
},
// 当取消授权或者打开设置授权
handleNoAuth(res) {
console.log("用户拒绝授权:", res);
// 因为在设置里授权摄像头不会立即生效,所以要返回上一页,确保重新进入当前页使摄像头生效
setTimeout(() => {
wx.navigateBack()
}, 500)
},
// 版本号过低的回调
handleCannotuse() {
console.log('版本号过低无法使用, 组件内已经弹窗提示过了');
wx.navigateBack()
},
// 视频录制完成
handleComplete(e) {
console.log('视频文件路径:', e.detail)
// e.detail: 视频临时路径
this.setData({ videoSrc: e.detail, isBack: false })
// 打印视频信息文件
wx.getFileInfo({
filePath: e.detail,
success: (res) => {
const { size } = res
console.log("视频文件大小M:", size / Math.pow(1024, 2));
},
fail: (err) => {
console.log("获取视频文件失败", err);
}
})
}
})
完整代码示例:github仓库
总结:
自定义的
组件向外触发了 noAuth用户未授权摄像录音、cannotUse不可使用、complete录制完成事件,你也可以自定义修改组件触发更多的事件。 wx.faceDetect只是进行人脸匹配检测,目前(2021.11)没有活体检测/身份识别功能,如果需要,简单的活体检测可以自己写写摇头抬头;或者更靠谱的上传人脸视频到后端处理/直接采用第三方的活体检测/身份识别接口功能。
wx.faceDetect是否存在平台兼容/手机差异问题?目前没发现,但社区有人遇到过问题,需要详细测试。
如果代码发现可优化,欢迎提issue改进。