|
@@ -1,29 +1,27 @@
|
|
|
-// components/camera-face/index.js
|
|
|
-
|
|
|
import { getAuthorize, setAuthorize, throttle, checkVersion } from './utils'
|
|
|
-
|
|
|
// 顶部提示信息
|
|
|
+// const app = getApp<IAppOption>();
|
|
|
const topTips = {
|
|
|
ready: '请正对手机,保持光线充足',
|
|
|
front: '请正对屏幕',
|
|
|
left: '请向左转头',
|
|
|
right: '请向右转头',
|
|
|
}
|
|
|
-
|
|
|
-// 底部提示信息
|
|
|
const bottomTips = {
|
|
|
- recording: '人脸录制中...',
|
|
|
- complete: '人脸录制成功',
|
|
|
- error: '人脸录制失败'
|
|
|
+ recording: '脸部信息采集中...',
|
|
|
+ complete: '脸部信息采集成功',
|
|
|
+ error: '脸部信息采集失败'
|
|
|
}
|
|
|
-
|
|
|
-const innerAudioContext = wx.createInnerAudioContext()
|
|
|
-let ctx: any = undefined;
|
|
|
-let listener: any = undefined;
|
|
|
-let interval: any = undefined;
|
|
|
+let ctx: WechatMiniprogram.CameraContext;
|
|
|
+let listener: WechatMiniprogram.CameraFrameListener;
|
|
|
+let startTime: number = 0;
|
|
|
+let innerAudioContext: WechatMiniprogram.InnerAudioContext;
|
|
|
|
|
|
Component({
|
|
|
-
|
|
|
+ ready() {
|
|
|
+ innerAudioContext = wx.createInnerAudioContext();
|
|
|
+ innerAudioContext.loop = false;
|
|
|
+ },
|
|
|
// 组件的属性列表
|
|
|
properties: {
|
|
|
// 人脸整体可信度 [0-1], 参考wx.faceDetect文档的res.confArray.global
|
|
@@ -68,296 +66,249 @@ Component({
|
|
|
type: String,
|
|
|
value: 'off'
|
|
|
},
|
|
|
- // 检测视频帧的节流时间,默认500毫秒执行一次
|
|
|
- throttleFrequency: {
|
|
|
- type: Number,
|
|
|
- value: 500
|
|
|
- }
|
|
|
},
|
|
|
|
|
|
// 组件页面的生命周期
|
|
|
pageLifetimes: {
|
|
|
// 页面被隐藏
|
|
|
hide: function () {
|
|
|
- this.stop()
|
|
|
+ this.stopRecord();
|
|
|
+ this.stopUI();
|
|
|
},
|
|
|
},
|
|
|
|
|
|
detached: function () {
|
|
|
// 在组件实例被从页面节点树移除时执行
|
|
|
- this.stop()
|
|
|
+ this.stopRecord();
|
|
|
+ this.stopUI();
|
|
|
},
|
|
|
|
|
|
+
|
|
|
+
|
|
|
// 组件的初始数据
|
|
|
data: {
|
|
|
- isReading: false, // 是否在准备中
|
|
|
- isRecoding: false, // 是否正在录制中
|
|
|
- isStopRecoding: false, // 是否正在停止录制中
|
|
|
-
|
|
|
- topTips: topTips.ready, // 顶部提示文字
|
|
|
- bottomTips: '', // 底部提示文字
|
|
|
- seconds: '',
|
|
|
- videoSrc: '',
|
|
|
- isCompleteRecoding: false
|
|
|
+ topTips: topTips.ready, // 顶部提示信息
|
|
|
+ bottomTips: "", //底部提示信息
|
|
|
+ gather: 0, // 采集状态:0 -未开始 1 -加载中 2 -录制中 3 -录制结束
|
|
|
+ seconds: 0
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 组件的方法列表
|
|
|
*/
|
|
|
methods: {
|
|
|
+ // 准备采集人脸信息
|
|
|
+ readyRecord() {
|
|
|
+ if (this.data.gather !== 0 && this.data.gather !== 3) return; // 开始采集
|
|
|
+ this.setData({ bottomTips: bottomTips.recording, gather: 1 }); // 状态转换为加载中
|
|
|
+ wx.nextTick(async () => {
|
|
|
+ wx.showLoading({ title: '加载中..', mask: true })
|
|
|
+ if (!checkVersion('2.18.0', () => this.triggerEvent('cannotUse'))) {
|
|
|
+ // 基础库不支持 faceDate
|
|
|
+ wx.hideLoading();
|
|
|
+ this.setData({ gather: 0, bottomTips: "" });
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- // 开启相机ctx
|
|
|
- async start() {
|
|
|
- const result = await this.initAuthorize()
|
|
|
- if (!result) return false
|
|
|
- if (!ctx) 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')
|
|
|
+ // 调用相机
|
|
|
+ await this.openCamera();
|
|
|
+ // 准备采集
|
|
|
+ startTime = Date.now();
|
|
|
+ this.initFace()
|
|
|
+ // 开始录制
|
|
|
+ this.startRecord()
|
|
|
})
|
|
|
- if (!canUse) {
|
|
|
- wx.hideLoading()
|
|
|
- this.setData({ isReading: false })
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // 启用相机
|
|
|
- try {
|
|
|
- const result = await this.start()
|
|
|
- if (!result || !ctx) throw new Error()
|
|
|
- } catch (e) {
|
|
|
- wx.hideLoading()
|
|
|
- this.setData({ isReading: false })
|
|
|
- return
|
|
|
- }
|
|
|
- console.log('准备录制')
|
|
|
- this.setData({ topTips: topTips.ready })
|
|
|
- // 视频帧回调节流函数
|
|
|
- let fn = throttle((frame: any) => {
|
|
|
- // 人脸识别
|
|
|
+ },
|
|
|
+ // init 人脸识别能力
|
|
|
+ initFace() {
|
|
|
+ // 视频帧数据读取节流
|
|
|
+ const fn = (frame: any) => {
|
|
|
+ const s = 15 - Math.floor((Date.now() - startTime) / 1000);
|
|
|
+ if (this.data.gather !== 2 || s % 500 > 100) return;
|
|
|
+ let tip = "";
|
|
|
+ switch (true) {
|
|
|
+ case s > 10:
|
|
|
+ tip = topTips.front
|
|
|
+ break;
|
|
|
+ case s > 5 && s <= 10:
|
|
|
+ tip = topTips.left;
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ tip = topTips.right;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ if (s == 15) innerAudioContext.src = "assets/voice/front.mp3";
|
|
|
+ if (s == 10) innerAudioContext.src = "assets/voice/left.mp3";
|
|
|
+ if (s == 5) innerAudioContext.src = "assets/voice/right.mp3";
|
|
|
+ if (s % 5 === 0 && s !== 0) innerAudioContext.play();
|
|
|
+ this.setData({
|
|
|
+ seconds: s,
|
|
|
+ topTips: tip
|
|
|
+ })
|
|
|
+ if (s <= 0) {
|
|
|
+ // 结束监听
|
|
|
+ this.stopRecord(true);// 停止录像逻辑
|
|
|
+ this.stopUI(true); // 重置ui逻辑;
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 识别人脸是否在画面种
|
|
|
wx.faceDetect({
|
|
|
frameBuffer: frame.data,
|
|
|
width: frame.width,
|
|
|
height: frame.height,
|
|
|
enableConf: true,
|
|
|
enableAngle: true,
|
|
|
- success: (res) => this.processFaceData(res),
|
|
|
- fail: () => this.cancel()
|
|
|
+ success: (res: WechatMiniprogram.FaceDetectSuccessCallbackResult) => this.processFaceData(res),
|
|
|
+ fail: (err) => {
|
|
|
+ wx.showToast({ title: '未识别到人脸', icon: 'error', duration: 2000 });
|
|
|
+ this.setData({
|
|
|
+ seconds: 0,
|
|
|
+ bottomTips: bottomTips.error
|
|
|
+ })
|
|
|
+ this.stopRecord();
|
|
|
+ this.stopUI();
|
|
|
+ }
|
|
|
})
|
|
|
- }, this.properties.throttleFrequency)
|
|
|
-
|
|
|
+ }
|
|
|
// 初始化人脸识别
|
|
|
wx.initFaceDetect({
|
|
|
success: () => {
|
|
|
+ wx.hideLoading();
|
|
|
listener = ctx.onCameraFrame((frame: any) => fn(frame))
|
|
|
- listener.start()
|
|
|
- },
|
|
|
- fail: (err) => {
|
|
|
- console.log('初始人脸识别失败', err)
|
|
|
- // this.setData({ topTips: '' })
|
|
|
- wx.showToast({ title: '未识别到人脸', icon: 'none' })
|
|
|
+ listener?.start()
|
|
|
},
|
|
|
- complete: () => {
|
|
|
- wx.hideLoading()
|
|
|
- this.setData({ isReading: false })
|
|
|
+ fail: () => {
|
|
|
+ wx.hideLoading();
|
|
|
+ // 初始化人脸识别失败
|
|
|
+ wx.showToast({ title: '未识别到人脸', icon: 'error', duration: 2000 });
|
|
|
+ this.setData({
|
|
|
+ seconds: 0,
|
|
|
+ bottomTips: bottomTips.error
|
|
|
+ })
|
|
|
+ this.stopRecord();
|
|
|
+ this.stopUI();
|
|
|
}
|
|
|
})
|
|
|
},
|
|
|
-
|
|
|
- // 处理人脸识别数据
|
|
|
- processFaceData(res: any) {
|
|
|
- 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
|
|
|
- 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()
|
|
|
+ // 人脸识别数据
|
|
|
+ processFaceData(res: WechatMiniprogram.FaceDetectSuccessCallbackResult) {
|
|
|
+ if (!res.confArray || !res.angleArray) {
|
|
|
+ // 人脸数据获取失败
|
|
|
+ wx.showToast({ title: '人脸数据获取失败', icon: 'error', duration: 2000 });
|
|
|
+ this.stopRecord();
|
|
|
+ this.stopUI();
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const { global } = res.confArray;
|
|
|
+ const { pitch, yaw, roll } = res.angleArray
|
|
|
+ const { p, y, r } = this.properties.faceAngle; // 偏移角度
|
|
|
+ const g = this.properties.faceCredibility;// 自定义可信度;
|
|
|
+ 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) {
|
|
|
+ // 人脸不可信,且非正脸
|
|
|
+ wx.showToast({ title: '未检测到人脸', icon: 'error', duration: 2000 });
|
|
|
+ this.setData({
|
|
|
+ seconds: 0,
|
|
|
+ bottomTips: bottomTips.error
|
|
|
+ })
|
|
|
+ this.stopRecord();
|
|
|
+ this.stopUI();
|
|
|
+ return
|
|
|
}
|
|
|
},
|
|
|
-
|
|
|
// 开始录制
|
|
|
startRecord() {
|
|
|
- console.log('开始录制')
|
|
|
ctx.startRecord({
|
|
|
+ timeout: 15,
|
|
|
success: () => {
|
|
|
- this.setRecordingTips()
|
|
|
- let timer = setTimeout(() => {
|
|
|
- this.completeRecord()
|
|
|
- clearTimeout(timer);
|
|
|
- }, this.properties.duration)
|
|
|
+ console.log("开始录制")
|
|
|
+ this.setData({ gather: 2, seconds: 15 });// 录制中
|
|
|
},
|
|
|
timeoutCallback: () => {
|
|
|
- // 超过30s或页面 onHide 时会结束录像
|
|
|
- this.stop()
|
|
|
+ if (this.data.gather !== 2) return;
|
|
|
+ this.stopRecord(); // 停止录像逻辑
|
|
|
+ this.stopUI(); // 重置ui逻辑;
|
|
|
+ }, // 超出录制时长
|
|
|
+ fail: () => {
|
|
|
+ this.stopRecord(); // 停止录像逻辑
|
|
|
+ this.stopUI(); // 重置ui逻辑;
|
|
|
},
|
|
|
- fail: () => this.stop()
|
|
|
- })
|
|
|
- },
|
|
|
-
|
|
|
- // 设置录制中的提示文字和倒计时
|
|
|
- setRecordingTips() {
|
|
|
- this.setData({
|
|
|
- bottomTips: bottomTips.recording
|
|
|
- })
|
|
|
-
|
|
|
- let second = (this.properties.duration / 1000)
|
|
|
- if (interval) interval = clearInterval(interval)
|
|
|
- interval = setInterval(() => {
|
|
|
- console.log('xxxxxx', second)
|
|
|
-
|
|
|
- // 音频播放
|
|
|
- if (second == 15) {
|
|
|
- innerAudioContext.src = "assets/voice/front.mp3"
|
|
|
- innerAudioContext.play()
|
|
|
- }
|
|
|
-
|
|
|
- else if (second == 10) {
|
|
|
- innerAudioContext.src = "assets/voice/left.mp3"
|
|
|
- innerAudioContext.play()
|
|
|
- }
|
|
|
-
|
|
|
- else if (second == 5) {
|
|
|
- innerAudioContext.src = "assets/voice/right.mp3"
|
|
|
- innerAudioContext.play()
|
|
|
- }
|
|
|
-
|
|
|
- // 修改提示文字
|
|
|
- if (second > 10)
|
|
|
- this.setData({
|
|
|
- topTips: topTips.front,
|
|
|
- })
|
|
|
- else if (second > 5 && second <= 10)
|
|
|
- this.setData({
|
|
|
- topTips: topTips.left,
|
|
|
- })
|
|
|
- else {
|
|
|
- this.setData({
|
|
|
- topTips: topTips.right,
|
|
|
- })
|
|
|
- }
|
|
|
- this.setData({
|
|
|
- seconds: second-- + 's'
|
|
|
- })
|
|
|
- if (second <= 0) {
|
|
|
- interval = clearInterval(interval)
|
|
|
- this.setData({
|
|
|
- seconds: '1s',
|
|
|
- topTips: '',
|
|
|
- bottomTips: bottomTips.complete
|
|
|
- })
|
|
|
+ complete: () => {
|
|
|
+ wx.hideLoading();
|
|
|
}
|
|
|
- }, 1000)
|
|
|
+ })
|
|
|
},
|
|
|
-
|
|
|
- // 完成录制
|
|
|
- completeRecord() {
|
|
|
- console.log('完成录制')
|
|
|
- this.setData({ isCompleteRecoding: true })
|
|
|
- ctx.stopRecord({
|
|
|
+ // 结束录制
|
|
|
+ stopRecord(isVadeo?: boolean) {
|
|
|
+ this.setData({ gather: isVadeo ? 3 : 0 });
|
|
|
+ console.log('+++++++++>')
|
|
|
+ ctx?.stopRecord({
|
|
|
compressed: this.properties.compressed,
|
|
|
- success: (res: any) => {
|
|
|
+ success: (res) => {
|
|
|
+ wx.hideLoading();
|
|
|
+ /**
|
|
|
+ * tempThumbPath 视频封面
|
|
|
+ * tempVideoPath 视频临时文件路径
|
|
|
+ */
|
|
|
+ console.log("录制结束", res, isVadeo);
|
|
|
+ if (!isVadeo) return;
|
|
|
+ this.triggerEvent('complete', res.tempVideoPath)
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.log("录制失败+++>", err);
|
|
|
+ wx.hideLoading();
|
|
|
+ wx.showToast({ title: "采集失败,请重试", icon: "none" });
|
|
|
+ wx.navigateBack();
|
|
|
this.setData({
|
|
|
- videoSrc: res.tempVideoPath
|
|
|
+ seconds: 0,
|
|
|
+ bottomTips: bottomTips.error
|
|
|
})
|
|
|
- // 向外触发完成录制的事件
|
|
|
- this.triggerEvent('complete', res.tempVideoPath)
|
|
|
- // this.uploadVideo()
|
|
|
},
|
|
|
- fail: () => this.stop(),
|
|
|
complete: () => {
|
|
|
- listener.stop()
|
|
|
- wx.stopFaceDetect()
|
|
|
- interval = clearInterval(interval);
|
|
|
- this.setData({ isCompleteRecoding: false })
|
|
|
+ console.log("取消录制");
|
|
|
+ listener?.stop();
|
|
|
+ wx.stopFaceDetect({});
|
|
|
}
|
|
|
})
|
|
|
-
|
|
|
},
|
|
|
-
|
|
|
- // 人脸移出等取消录制
|
|
|
- cancel() {
|
|
|
- console.log('取消录制')
|
|
|
- // 如果不在录制中或者正在录制完成中就不能取消
|
|
|
- if (!this.data.isRecoding || this.data.isCompleteRecoding) return
|
|
|
- interval = clearInterval(interval);
|
|
|
- ctx.stopRecord({
|
|
|
- complete: () => {
|
|
|
- console.log('取消录制成功')
|
|
|
- this.setData({
|
|
|
- topTips: topTips.ready,
|
|
|
- bottomTips: '',
|
|
|
- seconds: '',
|
|
|
- isRecoding: false
|
|
|
- })
|
|
|
- wx.showToast({ title: '录制失败,请重新录制', icon: 'none' })
|
|
|
- }
|
|
|
+ // 停止ui变化
|
|
|
+ stopUI(isEnd?: boolean) {
|
|
|
+ this.setData({
|
|
|
+ topTips: topTips.ready,
|
|
|
+ bottomTips: isEnd ? bottomTips.complete : bottomTips.error
|
|
|
})
|
|
|
},
|
|
|
-
|
|
|
- // 用户切入后台等停止使用摄像头
|
|
|
- stop() {
|
|
|
- console.log('停止录制')
|
|
|
- interval = clearInterval(interval);
|
|
|
- if (listener) listener.stop()
|
|
|
- if (ctx && !this.data.isCompleteRecoding) ctx.stopRecord()
|
|
|
- wx.stopFaceDetect()
|
|
|
- setTimeout(() => {
|
|
|
- this.setData({ topTips: '', isRecoding: false })
|
|
|
- }, 500)
|
|
|
- },
|
|
|
- // 用户不允许使用摄像头
|
|
|
- error() {
|
|
|
- // const cameraName = 'scope.camera';
|
|
|
- // this.triggerEvent('noAuth', cameraName)
|
|
|
+ // 开启相机
|
|
|
+ async openCamera() {
|
|
|
+ if (!(await this.initAuthorize())) {
|
|
|
+ this.setData({ gather: 0 });
|
|
|
+ throw new Error("未能正常启动摄像头");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ !ctx && (ctx = wx.createCameraContext());
|
|
|
+ } catch (error) {
|
|
|
+ wx.hideLoading()
|
|
|
+ wx.showToast({
|
|
|
+ title: "设备不支持视频采集",
|
|
|
+ icon: "error"
|
|
|
+ })
|
|
|
+ this.setData({ gather: 0 });
|
|
|
+ }
|
|
|
},
|
|
|
-
|
|
|
- // 初始相机和录音权限
|
|
|
+ // 初始化相机和录音权限
|
|
|
async initAuthorize() {
|
|
|
- const cameraName = 'scope.camera'
|
|
|
- const recordName = 'scope.record'
|
|
|
- const scopeCamera = await getAuthorize(cameraName)
|
|
|
- // 未授权相机
|
|
|
- if (!scopeCamera) {
|
|
|
- // 用户拒绝授权相机
|
|
|
- if (!(await setAuthorize(cameraName))) this.openSetting()
|
|
|
+ const noauthorize = !(await getAuthorize('scope.camera'))
|
|
|
+ // 相机或录音未授权
|
|
|
+ if (noauthorize) {
|
|
|
+ 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: '开启摄像头和录音权限',
|
|
@@ -365,12 +316,12 @@ Component({
|
|
|
content: '是否打开?',
|
|
|
success: (res) => {
|
|
|
this.triggerEvent('noAuth', '打开设置授权')
|
|
|
- if (res.confirm) {
|
|
|
- wx.openSetting()
|
|
|
- }
|
|
|
+ if (res.confirm) wx.openSetting();
|
|
|
}
|
|
|
})
|
|
|
- },
|
|
|
-
|
|
|
+ }
|
|
|
}
|
|
|
})
|
|
|
+
|
|
|
+
|
|
|
+export { }
|