index.ts 11 KB

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