index.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. import { getAuthorize, setAuthorize, throttle, checkVersion } from './utils'
  2. // 顶部提示信息
  3. // const app = getApp<IAppOption>();
  4. const topTips = {
  5. ready: '请正对手机,保持光线充足',
  6. front: '请正对屏幕',
  7. left: '请向左转头',
  8. right: '请向右转头',
  9. }
  10. const bottomTips = {
  11. recording: '脸部信息采集中...',
  12. complete: '脸部信息采集成功',
  13. error: '脸部信息采集失败'
  14. }
  15. let ctx: WechatMiniprogram.CameraContext;
  16. let listener: WechatMiniprogram.CameraFrameListener;
  17. let startTime: number = 0;
  18. let innerAudioContext: WechatMiniprogram.InnerAudioContext;
  19. Component({
  20. ready() {
  21. innerAudioContext = wx.createInnerAudioContext();
  22. innerAudioContext.loop = false;
  23. },
  24. // 组件的属性列表
  25. properties: {
  26. // 人脸整体可信度 [0-1], 参考wx.faceDetect文档的res.confArray.global
  27. // 当超过这个可信度且正脸时开始录制人脸, 反之停止录制
  28. faceCredibility: {
  29. type: Number,
  30. value: 0.3
  31. },
  32. // 人脸偏移角度正脸数值参考wx.faceDetect文档的res.angleArray
  33. // 越接近0越正脸,包括p仰俯角(pitch点头), y偏航角(yaw摇头), r翻滚角(roll左右倾)
  34. faceAngle: {
  35. type: Object,
  36. value: { p: 1, y: 1, r: 1 }
  37. },
  38. // 录制视频时长,不能超过30s
  39. duration: {
  40. type: Number,
  41. value: 15000
  42. },
  43. // 是否压缩视频
  44. compressed: {
  45. type: Boolean,
  46. value: false
  47. },
  48. // 前置或者后置 front,back
  49. devicePosition: {
  50. type: String,
  51. value: 'front'
  52. },
  53. // 指定期望的相机帧数据尺寸 small,medium,large
  54. frameSize: {
  55. type: String,
  56. value: 'medium'
  57. },
  58. // 分辨率 low,medium,high
  59. resolution: {
  60. type: String,
  61. value: 'medium'
  62. },
  63. // 闪光灯 auto,on,off,torch
  64. flash: {
  65. type: String,
  66. value: 'off'
  67. },
  68. },
  69. // 组件页面的生命周期
  70. pageLifetimes: {
  71. // 页面被隐藏
  72. hide: function () {
  73. this.stopRecord();
  74. this.stopUI();
  75. },
  76. },
  77. detached: function () {
  78. // 在组件实例被从页面节点树移除时执行
  79. this.stopRecord();
  80. this.stopUI();
  81. },
  82. // 组件的初始数据
  83. data: {
  84. topTips: topTips.ready, // 顶部提示信息
  85. bottomTips: "", //底部提示信息
  86. gather: 0, // 采集状态:0 -未开始 1 -加载中 2 -录制中 3 -录制结束
  87. seconds: 0
  88. },
  89. /**
  90. * 组件的方法列表
  91. */
  92. methods: {
  93. // 准备采集人脸信息
  94. readyRecord() {
  95. if (this.data.gather !== 0 && this.data.gather !== 3) return; // 开始采集
  96. this.setData({ bottomTips: bottomTips.recording, gather: 1 }); // 状态转换为加载中
  97. wx.nextTick(async () => {
  98. wx.showLoading({ title: '加载中..', mask: true })
  99. if (!checkVersion('2.18.0', () => this.triggerEvent('cannotUse'))) {
  100. // 基础库不支持 faceDate
  101. wx.hideLoading();
  102. this.setData({ gather: 0, bottomTips: "" });
  103. return;
  104. }
  105. // 调用相机
  106. await this.openCamera();
  107. // 准备采集
  108. startTime = Date.now();
  109. this.initFace()
  110. // 开始录制
  111. this.startRecord()
  112. })
  113. },
  114. // init 人脸识别能力
  115. initFace() {
  116. // 视频帧数据读取节流
  117. const fn = (frame: any) => {
  118. const s = 15 - Math.floor((Date.now() - startTime) / 1000);
  119. if (this.data.gather !== 2 || s % 500 > 100) return;
  120. let tip = "";
  121. switch (true) {
  122. case s > 10:
  123. tip = topTips.front
  124. break;
  125. case s > 5 && s <= 10:
  126. tip = topTips.left;
  127. break
  128. default:
  129. tip = topTips.right;
  130. break;
  131. }
  132. if (s == 15) innerAudioContext.src = "assets/voice/front.mp3";
  133. if (s == 10) innerAudioContext.src = "assets/voice/left.mp3";
  134. if (s == 5) innerAudioContext.src = "assets/voice/right.mp3";
  135. if (s % 5 === 0 && s !== 0) innerAudioContext.play();
  136. this.setData({
  137. seconds: s,
  138. topTips: tip
  139. })
  140. if (s <= 0) {
  141. // 结束监听
  142. this.stopRecord(true);// 停止录像逻辑
  143. this.stopUI(true); // 重置ui逻辑;
  144. return
  145. }
  146. // 识别人脸是否在画面种
  147. wx.faceDetect({
  148. frameBuffer: frame.data,
  149. width: frame.width,
  150. height: frame.height,
  151. enableConf: true,
  152. enableAngle: true,
  153. success: (res: WechatMiniprogram.FaceDetectSuccessCallbackResult) => this.processFaceData(res),
  154. fail: (err) => {
  155. wx.showToast({ title: '未识别到人脸', icon: 'error', duration: 2000 });
  156. this.setData({
  157. seconds: 0,
  158. bottomTips: bottomTips.error
  159. })
  160. this.stopRecord();
  161. this.stopUI();
  162. }
  163. })
  164. }
  165. // 初始化人脸识别
  166. wx.initFaceDetect({
  167. success: () => {
  168. wx.hideLoading();
  169. listener = ctx.onCameraFrame((frame: any) => fn(frame))
  170. listener?.start()
  171. },
  172. fail: () => {
  173. wx.hideLoading();
  174. // 初始化人脸识别失败
  175. wx.showToast({ title: '未识别到人脸', icon: 'error', duration: 2000 });
  176. this.setData({
  177. seconds: 0,
  178. bottomTips: bottomTips.error
  179. })
  180. this.stopRecord();
  181. this.stopUI();
  182. }
  183. })
  184. },
  185. // 人脸识别数据
  186. processFaceData(res: WechatMiniprogram.FaceDetectSuccessCallbackResult) {
  187. if (!res.confArray || !res.angleArray) {
  188. // 人脸数据获取失败
  189. wx.showToast({ title: '人脸数据获取失败', icon: 'error', duration: 2000 });
  190. this.stopRecord();
  191. this.stopUI();
  192. return
  193. }
  194. const { global } = res.confArray;
  195. const { pitch, yaw, roll } = res.angleArray
  196. const { p, y, r } = this.properties.faceAngle; // 偏移角度
  197. const g = this.properties.faceCredibility;// 自定义可信度;
  198. const isGlobal = global >= g
  199. const isPitch = Math.abs(pitch) <= p
  200. const isYaw = Math.abs(yaw) <= y
  201. const isRoll = Math.abs(roll) <= r
  202. if (!isGlobal || !isPitch || !isYaw || !isRoll) {
  203. // 人脸不可信,且非正脸
  204. wx.showToast({ title: '未检测到人脸', icon: 'error', duration: 2000 });
  205. this.setData({
  206. seconds: 0,
  207. bottomTips: bottomTips.error
  208. })
  209. this.stopRecord();
  210. this.stopUI();
  211. return
  212. }
  213. },
  214. // 开始录制
  215. startRecord() {
  216. ctx.startRecord({
  217. timeout: 15,
  218. success: () => {
  219. console.log("开始录制")
  220. this.setData({ gather: 2, seconds: 15 });// 录制中
  221. },
  222. timeoutCallback: () => {
  223. if (this.data.gather !== 2) return;
  224. this.stopRecord(); // 停止录像逻辑
  225. this.stopUI(); // 重置ui逻辑;
  226. }, // 超出录制时长
  227. fail: () => {
  228. this.stopRecord(); // 停止录像逻辑
  229. this.stopUI(); // 重置ui逻辑;
  230. },
  231. complete: () => {
  232. wx.hideLoading();
  233. }
  234. })
  235. },
  236. // 结束录制
  237. stopRecord(isVadeo?: boolean) {
  238. this.setData({ gather: isVadeo ? 3 : 0 });
  239. console.log('+++++++++>')
  240. ctx?.stopRecord({
  241. compressed: this.properties.compressed,
  242. success: (res) => {
  243. wx.hideLoading();
  244. /**
  245. * tempThumbPath 视频封面
  246. * tempVideoPath 视频临时文件路径
  247. */
  248. console.log("录制结束", res, isVadeo);
  249. if (!isVadeo) return;
  250. this.triggerEvent('complete', res.tempVideoPath)
  251. },
  252. fail: (err) => {
  253. console.log("录制失败+++>", err);
  254. wx.hideLoading();
  255. wx.showToast({ title: "采集失败,请重试", icon: "none" });
  256. wx.navigateBack();
  257. this.setData({
  258. seconds: 0,
  259. bottomTips: bottomTips.error
  260. })
  261. },
  262. complete: () => {
  263. console.log("取消录制");
  264. listener?.stop();
  265. wx.stopFaceDetect({});
  266. }
  267. })
  268. },
  269. // 停止ui变化
  270. stopUI(isEnd?: boolean) {
  271. this.setData({
  272. topTips: topTips.ready,
  273. bottomTips: isEnd ? bottomTips.complete : bottomTips.error
  274. })
  275. },
  276. // 开启相机
  277. async openCamera() {
  278. if (!(await this.initAuthorize())) {
  279. this.setData({ gather: 0 });
  280. throw new Error("未能正常启动摄像头");
  281. }
  282. try {
  283. !ctx && (ctx = wx.createCameraContext());
  284. } catch (error) {
  285. wx.hideLoading()
  286. wx.showToast({
  287. title: "设备不支持视频采集",
  288. icon: "error"
  289. })
  290. this.setData({ gather: 0 });
  291. }
  292. },
  293. // 初始化相机和录音权限
  294. async initAuthorize() {
  295. const noauthorize = !(await getAuthorize('scope.camera'))
  296. // 相机或录音未授权
  297. if (noauthorize) {
  298. this.openSetting();
  299. return false
  300. }
  301. return true
  302. },
  303. // 打开摄像头或录音权限授权框
  304. openSetting() {
  305. wx.showModal({
  306. title: '开启摄像头和录音权限',
  307. showCancel: true,
  308. content: '是否打开?',
  309. success: (res) => {
  310. this.triggerEvent('noAuth', '打开设置授权')
  311. if (res.confirm) wx.openSetting();
  312. }
  313. })
  314. }
  315. }
  316. })
  317. export { }