index.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. console.log(this.data.topTips)
  130. // 视频帧回调节流函数
  131. let fn = throttle((frame: any) => {
  132. // 人脸识别
  133. wx.faceDetect({
  134. frameBuffer: frame.data,
  135. width: frame.width,
  136. height: frame.height,
  137. enableConf: true,
  138. enableAngle: true,
  139. success: (res) => this.processFaceData(res),
  140. fail: () => this.cancel()
  141. })
  142. }, this.properties.throttleFrequency)
  143. // 初始化人脸识别
  144. wx.initFaceDetect({
  145. success: () => {
  146. listener = ctx.onCameraFrame((frame: any) => fn(frame))
  147. listener.start()
  148. },
  149. fail: (err) => {
  150. console.log('初始人脸识别失败', err)
  151. // this.setData({ topTips: '' })
  152. wx.showToast({ title: '未识别到人脸', icon: 'none' })
  153. },
  154. complete: () => {
  155. wx.hideLoading()
  156. this.setData({ isReading: false })
  157. }
  158. })
  159. },
  160. // 处理人脸识别数据
  161. processFaceData(res: any) {
  162. if (res.confArray && res.angleArray) {
  163. const { global } = res.confArray
  164. const g = this.properties.faceCredibility
  165. const { pitch, yaw, roll } = res.angleArray
  166. const { p, y, r } = this.properties.faceAngle
  167. console.log('res.confArray.global:', global)
  168. console.log('res.angleArray:', pitch, yaw, roll)
  169. const isGlobal = global >= g
  170. const isPitch = Math.abs(pitch) <= p
  171. const isYaw = Math.abs(yaw) <= y
  172. const isRoll = Math.abs(roll) <= r
  173. if (isGlobal && isPitch && isYaw && isRoll) {
  174. console.log('人脸可信,且是正脸')
  175. if (this.data.isRecoding || this.data.isCompleteRecoding) return
  176. this.setData({ isRecoding: true })
  177. this.startRecord() // 开始录制
  178. } else {
  179. console.log('人脸不可信,或者不是正脸')
  180. this.cancel()
  181. }
  182. } else {
  183. console.log('获取人脸识别数据失败', res)
  184. this.cancel()
  185. }
  186. },
  187. // 开始录制
  188. startRecord() {
  189. console.log('开始录制')
  190. ctx.startRecord({
  191. success: () => {
  192. this.setRecordingTips()
  193. let timer = setTimeout(() => {
  194. this.completeRecord()
  195. clearTimeout(timer);
  196. }, this.properties.duration)
  197. },
  198. timeoutCallback: () => {
  199. // 超过30s或页面 onHide 时会结束录像
  200. this.stop()
  201. },
  202. fail: () => this.stop()
  203. })
  204. },
  205. // 设置录制中的提示文字和倒计时
  206. setRecordingTips() {
  207. this.setData({
  208. bottomTips: bottomTips.recording
  209. })
  210. let second = (this.properties.duration / 1000)
  211. if (interval) interval = clearInterval(interval)
  212. interval = setInterval(() => {
  213. console.log('xxxxxx', second)
  214. // 音频播放
  215. if (second == 15) {
  216. innerAudioContext.src = "assets/voice/front.mp3"
  217. innerAudioContext.play()
  218. }
  219. else if (second == 10) {
  220. innerAudioContext.src = "assets/voice/left.mp3"
  221. innerAudioContext.play()
  222. }
  223. else if (second == 5) {
  224. innerAudioContext.src = "assets/voice/right.mp3"
  225. innerAudioContext.play()
  226. }
  227. // 修改提示文字
  228. if (second > 10)
  229. this.setData({
  230. topTips: topTips.front,
  231. })
  232. else if (second > 5 && second <= 10)
  233. this.setData({
  234. topTips: topTips.left,
  235. })
  236. else {
  237. this.setData({
  238. topTips: topTips.right,
  239. })
  240. }
  241. this.setData({
  242. seconds: second-- + 's'
  243. })
  244. if (second <= 0) {
  245. interval = clearInterval(interval)
  246. this.setData({
  247. seconds: '1s',
  248. topTips: '',
  249. bottomTips: bottomTips.complete
  250. })
  251. }
  252. }, 1000)
  253. },
  254. // 完成录制
  255. completeRecord() {
  256. console.log('完成录制')
  257. this.setData({ isCompleteRecoding: true })
  258. ctx.stopRecord({
  259. compressed: this.properties.compressed,
  260. success: (res: any) => {
  261. this.setData({
  262. videoSrc: res.tempVideoPath
  263. })
  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' })
  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. })