index copy.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. // components/camera-face/index.js
  2. import { getAuthorize, setAuthorize, throttle, checkVersion } from './utils'
  3. // 顶部提示信息
  4. const topTips = {
  5. ready: '请正对手机,保持光线充足',
  6. front: '请正对屏幕',
  7. left: '请向左转头',
  8. right: '请向右转头',
  9. }
  10. // 底部提示信息
  11. const bottomTips = {
  12. recording: '脸部信息录入中...',
  13. complete: '脸部信息录入成功',
  14. error: '脸部信息录入失败'
  15. }
  16. const innerAudioContext = wx.createInnerAudioContext()
  17. let ctx: any = undefined;
  18. let listener: any = undefined;
  19. let interval: any = undefined;
  20. Component({
  21. // 组件的属性列表
  22. properties: {
  23. // 人脸整体可信度 [0-1], 参考wx.faceDetect文档的res.confArray.global
  24. // 当超过这个可信度且正脸时开始录制人脸, 反之停止录制
  25. faceCredibility: {
  26. type: Number,
  27. value: 0.3
  28. },
  29. // 人脸偏移角度正脸数值参考wx.faceDetect文档的res.angleArray
  30. // 越接近0越正脸,包括p仰俯角(pitch点头), y偏航角(yaw摇头), r翻滚角(roll左右倾)
  31. faceAngle: {
  32. type: Object,
  33. value: { p: 1, y: 1, r: 1 }
  34. },
  35. // 录制视频时长,不能超过30s
  36. duration: {
  37. type: Number,
  38. value: 15000
  39. },
  40. // 是否压缩视频
  41. compressed: {
  42. type: Boolean,
  43. value: false
  44. },
  45. // 前置或者后置 front,back
  46. devicePosition: {
  47. type: String,
  48. value: 'front'
  49. },
  50. // 指定期望的相机帧数据尺寸 small,medium,large
  51. frameSize: {
  52. type: String,
  53. value: 'medium'
  54. },
  55. // 分辨率 low,medium,high
  56. resolution: {
  57. type: String,
  58. value: 'medium'
  59. },
  60. // 闪光灯 auto,on,off,torch
  61. flash: {
  62. type: String,
  63. value: 'off'
  64. },
  65. // 检测视频帧的节流时间,默认500毫秒执行一次
  66. throttleFrequency: {
  67. type: Number,
  68. value: 500
  69. }
  70. },
  71. // 组件页面的生命周期
  72. pageLifetimes: {
  73. // 页面被隐藏
  74. hide: function () {
  75. this.stop()
  76. },
  77. },
  78. detached: function () {
  79. // 在组件实例被从页面节点树移除时执行
  80. this.stop()
  81. },
  82. // 组件的初始数据
  83. data: {
  84. isReading: false, // 是否在准备中
  85. isRecoding: false, // 是否正在录制中
  86. isStopRecoding: false, // 是否正在停止录制中
  87. topTips: topTips.ready, // 顶部提示文字
  88. bottomTips: '', // 底部提示文字
  89. seconds: '',
  90. videoSrc: '',
  91. isCompleteRecoding: false
  92. },
  93. /**
  94. * 组件的方法列表
  95. */
  96. methods: {
  97. // 开启相机ctx
  98. async start() {
  99. const result = await this.initAuthorize()
  100. if (!result) return false
  101. if (!ctx) ctx = wx.createCameraContext()
  102. return true
  103. },
  104. // 准备录制
  105. async readyRecord() {
  106. if (this.data.isReading) return
  107. this.setData({ isReading: true })
  108. wx.showLoading({ title: '加载中..', mask: true })
  109. // 检测版本号
  110. const canUse = checkVersion('2.18.0', () => {
  111. this.triggerEvent('cannotUse')
  112. })
  113. if (!canUse) {
  114. wx.hideLoading()
  115. this.setData({ isReading: false })
  116. return
  117. }
  118. // 启用相机
  119. try {
  120. const result = await this.start()
  121. if (!result || !ctx) throw new Error()
  122. } catch (e) {
  123. wx.hideLoading()
  124. this.setData({ isReading: false })
  125. return
  126. }
  127. console.log('准备录制');
  128. this.setData({ topTips: topTips.ready })
  129. // 视频帧回调节流函数
  130. let fn = throttle((frame: any) => {
  131. // 人脸识别
  132. wx.faceDetect({
  133. frameBuffer: frame.data,
  134. width: frame.width,
  135. height: frame.height,
  136. enableConf: true,
  137. enableAngle: true,
  138. success: (res) => this.processFaceData(res),
  139. fail: () => this.cancel()
  140. })
  141. }, this.properties.throttleFrequency)
  142. // 初始化人脸识别
  143. wx.initFaceDetect({
  144. success: () => {
  145. wx.hideLoading()
  146. listener = ctx.onCameraFrame((frame: any) => fn(frame))
  147. listener.start()
  148. },
  149. fail: (err) => {
  150. wx.hideLoading()
  151. console.log('初始人脸识别失败', err)
  152. // this.setData({ topTips: '' })
  153. wx.showToast({ title: '未识别到人脸', icon: 'none', duration: 2000 });
  154. this.stop();
  155. },
  156. complete: () => {
  157. this.setData({ isReading: false })
  158. }
  159. })
  160. },
  161. // 处理人脸识别数据
  162. processFaceData(res: any) {
  163. if (res.confArray && res.angleArray) {
  164. const { global } = res.confArray
  165. const g = this.properties.faceCredibility
  166. const { pitch, yaw, roll } = res.angleArray
  167. const { p, y, r } = this.properties.faceAngle
  168. const isGlobal = global >= g
  169. const isPitch = Math.abs(pitch) <= p
  170. const isYaw = Math.abs(yaw) <= y
  171. const isRoll = Math.abs(roll) <= r
  172. if (isGlobal && isPitch && isYaw && isRoll) {
  173. console.log('人脸可信,且是正脸')
  174. if (this.data.isRecoding || this.data.isCompleteRecoding) return
  175. this.setData({ isRecoding: true })
  176. this.startRecord() // 开始录制
  177. } else {
  178. console.log('人脸不可信,或者不是正脸')
  179. this.cancel()
  180. }
  181. } else {
  182. console.log('获取人脸识别数据失败', res)
  183. this.cancel()
  184. }
  185. },
  186. // 开始录制
  187. startRecord() {
  188. console.log('开始录制')
  189. ctx.startRecord({
  190. success: () => {
  191. this.setRecordingTips()
  192. let timer = setTimeout(() => {
  193. this.completeRecord()
  194. clearTimeout(timer);
  195. }, this.properties.duration)
  196. },
  197. timeoutCallback: () => {
  198. // 超过30s或页面 onHide 时会结束录像
  199. this.stop()
  200. },
  201. fail: () => this.stop()
  202. })
  203. },
  204. // 设置录制中的提示文字和倒计时
  205. setRecordingTips() {
  206. this.setData({
  207. bottomTips: bottomTips.recording
  208. })
  209. let second = (this.properties.duration / 1000)
  210. if (interval) interval = clearInterval(interval)
  211. interval = setInterval(() => {
  212. console.log('xxxxxx', second)
  213. // 音频播放
  214. if (second == 15) {
  215. innerAudioContext.src = "assets/voice/front.mp3"
  216. innerAudioContext.play()
  217. }
  218. else if (second == 10) {
  219. innerAudioContext.src = "assets/voice/left.mp3"
  220. innerAudioContext.play()
  221. }
  222. else if (second == 5) {
  223. innerAudioContext.src = "assets/voice/right.mp3"
  224. innerAudioContext.play()
  225. }
  226. // 修改提示文字
  227. if (second > 10)
  228. this.setData({
  229. topTips: topTips.front,
  230. })
  231. else if (second > 5 && second <= 10)
  232. this.setData({
  233. topTips: topTips.left,
  234. })
  235. else {
  236. this.setData({
  237. topTips: topTips.right,
  238. })
  239. }
  240. this.setData({
  241. seconds: second-- + 's'
  242. })
  243. if (second <= 0) {
  244. interval = clearInterval(interval)
  245. this.setData({
  246. seconds: '1s',
  247. topTips: topTips.ready,
  248. bottomTips: bottomTips.complete
  249. })
  250. }
  251. }, 1000)
  252. },
  253. // 完成录制
  254. completeRecord() {
  255. console.log('完成录制')
  256. this.setData({ isCompleteRecoding: true })
  257. ctx.stopRecord({
  258. compressed: this.properties.compressed,
  259. success: (res: any) => {
  260. this.setData({
  261. videoSrc: res.tempVideoPath
  262. })
  263. console.log("++++++++")
  264. // 向外触发完成录制的事件
  265. this.triggerEvent('complete', res.tempVideoPath)
  266. // this.uploadVideo()
  267. },
  268. fail: () => this.stop(),
  269. complete: () => {
  270. listener.stop()
  271. wx.stopFaceDetect()
  272. interval = clearInterval(interval);
  273. this.setData({ isCompleteRecoding: false })
  274. }
  275. })
  276. },
  277. // 人脸移出等取消录制
  278. cancel() {
  279. console.log('取消录制')
  280. // 如果不在录制中或者正在录制完成中就不能取消
  281. if (!this.data.isRecoding || this.data.isCompleteRecoding) return
  282. interval = clearInterval(interval);
  283. ctx.stopRecord({
  284. complete: () => {
  285. console.log('取消录制成功')
  286. this.setData({
  287. topTips: topTips.ready,
  288. bottomTips: '',
  289. seconds: '',
  290. isRecoding: false
  291. })
  292. wx.showToast({ title: '录制失败,请重新录制', icon: 'none', duration: 2000 })
  293. }
  294. })
  295. },
  296. // 用户切入后台等停止使用摄像头
  297. stop() {
  298. console.log('停止录制')
  299. interval = clearInterval(interval);
  300. if (listener) listener.stop()
  301. if (ctx && !this.data.isCompleteRecoding) ctx.stopRecord()
  302. wx.stopFaceDetect()
  303. setTimeout(() => {
  304. this.setData({ topTips: '', isRecoding: false })
  305. }, 500)
  306. },
  307. // 用户不允许使用摄像头
  308. error() {
  309. // const cameraName = 'scope.camera';
  310. // this.triggerEvent('noAuth', cameraName)
  311. },
  312. // 初始相机和录音权限
  313. async initAuthorize() {
  314. const cameraName = 'scope.camera'
  315. const recordName = 'scope.record'
  316. const scopeCamera = await getAuthorize(cameraName)
  317. // 未授权相机
  318. if (!scopeCamera) {
  319. // 用户拒绝授权相机
  320. if (!(await setAuthorize(cameraName))) this.openSetting()
  321. return false
  322. }
  323. const scopeRecord = await getAuthorize(recordName)
  324. if (!scopeRecord) {
  325. // 用户拒绝授权录音
  326. if (!(await setAuthorize(recordName))) {
  327. this.openSetting()
  328. return false
  329. }
  330. }
  331. return true
  332. },
  333. // 打开设置授权
  334. openSetting() {
  335. wx.showModal({
  336. title: '开启摄像头和录音权限',
  337. showCancel: true,
  338. content: '是否打开?',
  339. success: (res) => {
  340. this.triggerEvent('noAuth', '打开设置授权')
  341. if (res.confirm) {
  342. wx.openSetting()
  343. }
  344. }
  345. })
  346. },
  347. }
  348. })