最近的项目开发中做了一个微信小程序录音的功能,遇到了一些问题,是如何解决的?
本文会从两种解决方案(微信小程序录音API/H5录音嵌入webview中),从优点、难点、缺陷、实现等进行讲解,并附有代码。
RecorderManager
微信小程序 API
创建全局的唯一录音器,可以在当前页面或者小程序内其他页面操作,不影响录音运行。
缺陷
- 熄屏、锁屏后会停止录音
- 切换 APP 也会停止录音
- 小程序进入后台并被「挂起」后,如果很长时间(目前是 30 分钟)都未再次进入前台,小程序会被销毁;也会停止录音
难点
- 录音切换页面、小程序后台运行、微信后台运行,会暂停录音,如何保持联系
- 录音时长最大支持10分钟,解决方法
实现
创建全局的录音管理器,通过
start
开始录音,stop
结束录音;并在onStop
监听结束录音获取到录音文件并做处理。
API 使用
RecorderManager
全局唯一的录音管理器RecorderManager.start
开始录音RecorderManager.stop
停止录音RecorderManager.resume
继续录音RecorderManager.onPause
监听录音暂停;如非手动暂停,可在此继续录音RecorderManager.onStop
监听录音结束,会返回录音的临时文件tempFilePath
录音流程
代码示例
本示例是通过 react + Taro 自定义 hook,可直接获取使用。
1import Taro, {
2 getSetting,
3 authorize,
4 openSetting,
5 showModal,
6} from '@tarojs/taro'
7import { useEffect, useRef } from 'react'
8
9// 录音最大时长
10const maxDuration = 1000 * 60 * 10
11// 录音默认配置
12const defaultOptions = {
13 duration: maxDuration,
14 format: 'mp3', // 文件格式
15 numberOfChannels: 1,
16}
17// 全局的录音管理器
18const recorderManager = Taro.getRecorderManager()
19
20/**
21 * 拜访打卡任务录音
22 * @param {*} options 音频配置,见小程序文档
23 * @param {*} onUploadSuccess 自定义上传回调
24 * @param {*} isContinueRecord 超过10分钟是否继续录制,暂未实现
25 * @returns
26 */
27const useRecord = ({ options = {}, onUploadSuccess, isContinueRecord = false } = {}) => {
28 // 是否已开始录音
29 let isRecording = false
30 // 是否手动结束
31 let isManualEnd = false
32 // 继续录音失败
33 let resuumeFial = false
34
35 /**
36 * 开启/关闭屏幕常亮,防止录音时熄屏后录音失败
37 */
38 const setKeepScreenOn = (state = false) => {
39 Taro.setKeepScreenOn({ keepScreenOn: state })
40 }
41
42 /**
43 * 初始化全局录音组件
44 */
45 const initRecorderManager = () => {
46 // 录音开始
47 recorderManager.onStart(() => {
48 resuumeFial = false
49 // 开始录音后,设置屏幕常亮;防止熄屏后录音失败
50 setKeepScreenOn(true)
51 })
52
53 // 录音失败
54 recorderManager.onError((res) => {
55 // 锁屏或者息屏后,继续录音失败
56 if (res.errMsg.includes('resume') && res.errMsg.includes('fail')) {
57 resuumeFial = true
58 // 手动停止
59 recorderManager.stop()
60 }
61 setKeepScreenOn(false)
62 isRecording = false
63 isManualEnd = false
64 })
65
66 // 录音结束
67 recorderManager.onStop((res) => {
68 // 关闭屏幕常亮设置
69 setKeepScreenOn(false)
70 // 录音时间小于1秒不做处理。
71 if (res.duration < 1000) {
72 Taro.showToast('录音时长需大于1秒')
73 isRecording = false
74 return
75 }
76 /** 手动停止录音 || 录音时间超过 10 分钟,触发上传文件 */
77 if (isContinueRecord && res.duration >= maxDuration && !isManualEnd) {
78 // 超过10分钟录音,再次开始录音
79 continueRecord()
80 } else if (!resuumeFial) {
81 // 手动停止录音
82 onUploadSuccess(res)
83 isManualEnd = false
84 }
85 })
86
87 // 录音暂停
88 recorderManager.onPause(() => {
89 // 录音暂停后,继续录音
90 if (isRecording) recorderManager.resume()
91 })
92
93 // 录音继续
94 recorderManager.onResume(() => {
95 resuumeFial = false
96 })
97
98 // 中断结束事件
99 recorderManager.onInterruptionEnd(() => {
100 // 继续录音
101 recorderManager.resume()
102 })
103 }
104
105 /**
106 * 开始录音
107 */
108 const startRecord = () => {
109 if (isRecording) return
110 isRecording = true
111 initRecorderManager()
112 recorderManager.start({
113 ...defaultOptions,
114 ...options,
115 })
116 }
117
118 /**
119 * 再次录音,用于超过10分钟后再次触发录音
120 */
121 const continueRecord = () => {
122 recorderManager.start({
123 ...defaultOptions,
124 ...options,
125 })
126 }
127
128 /**
129 * 结束录音,录音上传
130 */
131 const stopRecord = () => {
132 isManualEnd = true
133 recorderManager.stop()
134 }
135
136 /**
137 * 获取录音权限
138 */
139 function getRecordAuth() {
140 return new Promise((resolve, reject) => {
141 getSetting({
142 success: (res) => {
143 resolve(!!res.authSetting['scope.record'])
144 },
145 fail: (err) => {
146 reject(err)
147 },
148 })
149 })
150 }
151
152 /**
153 * 打开设置,授权录音权限
154 */
155 function openRecordAuth() {
156 authorize({
157 scope: 'scope.record',
158 success: () => {},
159 fail: () => {
160 showModal({
161 title: '录音需开启麦克风权限',
162 confirmText: '前往开启',
163 success: (data) => {
164 if (data.confirm) {
165 openSetting()
166 } else if (data.cancel) {
167 Taro.showToast('授权失败')
168 }
169 },
170 })
171 },
172 })
173 }
174
175 return {
176 startRecord,
177 stopRecord,
178 getRecordAuth,
179 openRecordAuth,
180 }
181}
182
183export default useRecord
184
MediaRecorder
MediaStream Recording API
- 通过开发录音的 H5 并嵌入 webview 可以解决熄屏、锁屏、切换 APP的录音暂停问题。
- 不仅可以解决上述小程序的缺陷;而且还可以解决不同端的录音器 API 差异或没有支持录音 API。
缺陷
- 小程序 “返回上一页” 等销毁 webview 页面的操作,录音也会被停止
难点
- H5 的与小程序 webview 通信
- H5 录音,拒绝授权后的引导处理
实现
- 通过 MediaRecorder 实现录音的H5页面。
- 需要在小程序中使用,使用
webview
嵌入H5,然后通过 postMessage 进行通信。
兼容性
MediaRecorder API,目前的主流浏览器都已经支持,包括微信浏览器。
API
const stream = awwit navigator.mediaDevices.getUserMedia({ audio: true })
打开浏览器麦克风录音new MediaRecorder(stream, { mimeTyp: 'audio/mp3', })
创建录音器ondataavailable
用于获取录制的媒体资源start
开始录音stop
结束录音
代码
本示例是通过 react 自定义 hook,可直接获取使用。
1import { useEffect, useRef } from 'react'
2
3/**
4 * useRecord
5 * @param {*} isStart 是否初始化完成就开始录音
6 * @param {*} maxRecordTime 录音最大时长,单位秒,默认10分钟
7 * @param {*} onUploadSuccess 录音结束回调
8 * @param {*} timeChange 录音时长
9 * @returns
10 */
11const useRecord = ({ isStart, maxRecordTime = 10 * 60, onUploadSuccess, timeChange }) => {
12 // 录音器
13 const mediaRecorder = useRef(null)
14 // 录音去加载完成,开始开始录音
15 let isStartRecord = isStart || false
16 // 录音时长
17 let recordTime = 0
18 // 计时器
19 let timer = null
20
21 /**
22 * 记录录音时长
23 * @returns
24 */
25 const handleRecordTime = (type) => {
26 if (type === 'stop') {
27 timeChange?.({ time: recordTime, state: mediaRecorder.current?.state })
28 return clearInterval(timer)
29 }
30 timer = setInterval(() => {
31 if (recordTime >= maxRecordTime) {
32 onStop()
33 timeChange?.({ time: recordTime, state: 'inactive' })
34 clearInterval(timer)
35 } else {
36 recordTime += 1
37 timeChange?.({ time: recordTime, state: mediaRecorder.current?.state })
38 }
39 }, 1000)
40 }
41
42 // 录音初始化
43 const initMediaRecorder = async () => {
44 if (!navigator?.mediaDevices) {
45 alert('该浏览器不支持录音(mediaDevices)')
46 return
47 }
48 navigator.mediaDevices
49 .getUserMedia({ audio: true })
50 .then((stream) => {
51 mediaRecorder.current = new MediaRecorder(stream, {
52 mimeTyp: 'audio/mp3',
53 })
54 // 开始录音
55 mediaRecorder.current.onstart = function (e) {
56 recordTime = 0
57 handleRecordTime('start')
58 }
59 // 录音暂停
60 mediaRecorder.current.onpause = function (e) {
61 handleRecordTime('stop')
62 }
63 // 录音继续
64 mediaRecorder.current.onresume = function (e) {
65 handleRecordTime('start')
66 }
67 // 录音结束
68 mediaRecorder.current.onstop = function (e) {
69 handleRecordTime('stop')
70 }
71 // 录音错误
72 mediaRecorder.current.onerror = function (e) {
73 handleRecordTime('stop')
74 }
75 // 录制的资源,录音结束才会触发
76 mediaRecorder.current.ondataavailable = function (e) {
77 onUploadSuccess(e)
78 }
79
80 // 进入页面,录音器加载完后就开始录音
81 if (isStartRecord) {
82 onStart()
83 isStartRecord = false
84 }
85 })
86 .catch((err) => {
87 if (
88 err.toString().includes('denied') &&
89 (err.toString().includes('Permission') || err.toString().includes('permission'))
90 ) {
91 alert('录音授权失败,请清除缓存后再操作')
92 return
93 }
94 alert(err)
95 })
96 }
97
98 // 开始录音
99 const onStart = async () => {
100 // state: inactive -未开始, recording - 录音中,paused - 录音暂停
101 if (mediaRecorder?.current?.state === 'inactive') mediaRecorder.current?.start()
102 }
103
104 // 结束录音
105 const onStop = () => {
106 if (mediaRecorder.current && mediaRecorder.current?.state !== 'inactive') mediaRecorder.current.stop()
107 }
108
109 // 继续录音
110 // isload 页面初次加载完成后开始录音
111 const onContinue = async (isload) => {
112 if (mediaRecorder?.current?.state === 'paused' && recordTime) mediaRecorder.current.resume()
113 else if (!mediaRecorder?.current && isload && !recordTime) isStartRecord = true
114 else if (!recordTime) onStart()
115 }
116
117 // 暂停录音
118 const onPause = () => {
119 if (mediaRecorder?.current?.state === 'recording') mediaRecorder.current.pause()
120 }
121
122 // 录音兼容问题处理
123 const initUserMedia = () => {
124 // eslint-disable-next-line no-undef
125 if (navigator.mediaDevices === undefined) {
126 // eslint-disable-next-line no-undef
127 navigator.mediaDevices = {}
128 }
129
130 // eslint-disable-next-line no-undef
131 if (navigator.mediaDevices.getUserMedia === undefined) {
132 // eslint-disable-next-line no-undef
133 navigator.mediaDevices.getUserMedia = function (constraints) {
134 // eslint-disable-next-line no-undef
135 const getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia
136
137 if (!getUserMedia) {
138 return Promise.reject(new Error('浏览器不支持 getUserMedia !'))
139 }
140
141 return new Promise((resolve, reject) => {
142 // eslint-disable-next-line no-undef
143 getUserMedia.call(navigator, constraints, resolve, reject)
144 })
145 }
146 }
147 }
148
149 /**
150 * init
151 */
152 const initRecord = async () => {
153 await initUserMedia()
154 initMediaRecorder()
155 }
156
157 useEffect(() => {
158 initRecord()
159 }, [])
160
161 return {
162 onContinue,
163 onStart,
164 onStop,
165 onPause,
166 }
167}
168
169export default useRecord
注意事项
- 录音的 H5 网络协议得是
https
协议 - 录音最好设置最大时长;时长过大,录音文件大小会比较大,上传速度会有影响
- 嵌入到小程序中,需配置
webview
域名 - 与
webview
进行通信,在onMessage
获取数据;返回的数据是多次postMessage
的数据组成的数组。 - 在 H5 中通过
wxjsddk
进行操作,postMessage
进行数据通信,只会在小程序后退、组件销毁、分享、复制链接后触发。 - 更多请查看API:developers.weixin.qq.com/miniprogram…
65417ff4acff4902c7404ee344ecd8a2.png
个人笔记记录 2021 ~ 2025