引言
最近的项目开发中做了一个微信小程序录音的功能,遇到了一些问题,是如何解决的?
本文会从两种解决方案(微信小程序录音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,可直接获取使用。
import Taro, {
getSetting,
authorize,
openSetting,
showModal,
} from '@tarojs/taro';
import { useEffect, useRef } from 'react';
// 录音最大时长
const maxDuration = 1000 * 60 * 10; // 时长,小程序仅支持 10 分钟录音(600000ms)
// 录音默认配置
const defaultOptions = {
duration: maxDuration,
format: 'mp3', // 文件格式
numberOfChannels: 1,
};
// 全局的录音管理器
const recorderManager = Taro.getRecorderManager();
/**
* 拜访打卡任务录音
* @param {*} options 音频配置,见小程序文档
* @param {*} onUploadSuccess 自定义上传回调
* @param {*} isContinueRecord 超过10分钟是否继续录制,暂未实现
* @returns
*/
const useRecord = ({ options = {}, onUploadSuccess, isContinueRecord = false } = {}) => {
// 是否已开始录音
let isRecording = false;
// 是否手动结束
let isManualEnd = false;
// 继续录音失败
let resuumeFial = false;
/**
* 开启/关闭屏幕常亮,防止录音时熄屏后录音失败
*/
const setKeepScreenOn = (state = false) => {
Taro.setKeepScreenOn({ keepScreenOn: state });
};
/**
* 初始化全局录音组件
*/
const initRecorderManager = () => {
// 录音开始
recorderManager.onStart(() => {
resuumeFial = false;
// 开始录音后,设置屏幕常亮;防止熄屏后录音失败
setKeepScreenOn(true);
});
// 录音失败
recorderManager.onError((res) => {
// 锁屏或者息屏后,继续录音失败
if (res.errMsg.includes('resume') && res.errMsg.includes('fail')) {
resuumeFial = true;
// 手动停止
recorderManager.stop();
}
setKeepScreenOn(false);
isRecording = false;
isManualEnd = false;
});
// 录音结束
recorderManager.onStop((res) => {
// 关闭屏幕常亮设置
setKeepScreenOn(false);
// 录音时间小于1秒不做处理。
if (res.duration < 1000) {
Taro.showToast('录音时长需大于1秒');
isRecording = false;
return;
}
/** 手动停止录音 || 录音时间超过 10 分钟,触发上传文件 */
if (isContinueRecord && res.duration >= maxDuration && !isManualEnd) {
// 超过10分钟录音,再次开始录音
continueRecord();
} else if (!resuumeFial) {
// 手动停止录音
onUploadSuccess(res);
isManualEnd = false;
}
});
// 录音暂停
recorderManager.onPause(() => {
// 录音暂停后,继续录音
if (isRecording) recorderManager.resume();
});
// 录音继续
recorderManager.onResume(() => {
resuumeFial = false;
});
// 中断结束事件
recorderManager.onInterruptionEnd(() => {
// 继续录音
recorderManager.resume();
});
};
/**
* 开始录音
*/
const startRecord = () => {
if (isRecording) return;
isRecording = true;
initRecorderManager();
recorderManager.start({
...defaultOptions,
...options,
});
};
/**
* 再次录音,用于超过10分钟后再次触发录音
*/
const continueRecord = () => {
recorderManager.start({
...defaultOptions,
...options,
});
};
/**
* 结束录音,录音上传
*/
const stopRecord = () => {
isManualEnd = true;
recorderManager.stop(); // 结束录音
};
/**
* 获取录音权限
*/
function getRecordAuth() {
return new Promise((resolve, reject) => {
getSetting({
success: (res) => {
resolve(!!res.authSetting['scope.record']);
},
fail: (err) => {
reject(err);
},
});
});
}
/**
* 打开设置,授权录音权限
*/
function openRecordAuth() {
authorize({
scope: 'scope.record',
success: () => {},
fail: () => {
showModal({
title: '录音需开启麦克风权限',
confirmText: '前往开启',
success: (data) => {
if (data.confirm) {
openSetting();
} else if (data.cancel) {
Taro.showToast('授权失败');
}
},
});
},
});
}
return {
startRecord,
stopRecord,
getRecordAuth,
openRecordAuth,
};
};
export default useRecord;
方案二
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,可直接获取使用。
import { useEffect, useRef } from 'react';
/**
* useRecord
* @param {*} isStart 是否初始化完成就开始录音
* @param {*} maxRecordTime 录音最大时长,单位秒,默认10分钟
* @param {*} onUploadSuccess 录音结束回调
* @param {*} timeChange 录音时长
* @returns
*/
const useRecord = ({ isStart, maxRecordTime = 10 * 60, onUploadSuccess, timeChange }) => {
// 录音器
const mediaRecorder = useRef(null);
// 录音去加载完成,开始开始录音
let isStartRecord = isStart || false;
// 录音时长
let recordTime = 0;
// 计时器
let timer = null;
// 录音资源
let dataBuffer = [];
// 清除录音资源
const dataReset = () => {
dataBuffer = [];
};
/**
* 记录录音时长
*/
const handleRecordTime = (type) => {
if (type === 'stop') {
timeChange?.({ time: recordTime, state: mediaRecorder.current?.state });
return clearInterval(timer);
}
timer = setInterval(() => {
if (recordTime >= maxRecordTime) {
onStop();
timeChange?.({ time: recordTime, state: 'inactive' });
clearInterval(timer);
} else {
recordTime += 1;
timeChange?.({ time: recordTime, state: mediaRecorder.current?.state });
}
}, 1000);
};
// 录音初始化
const initMediaRecorder = async () => {
if (!navigator?.mediaDevices) {
alert('该浏览器不支持录音(mediaDevices)');
return;
}
navigator.mediaDevices
.getUserMedia({ audio: true })
.then((stream) => {
mediaRecorder.current = new MediaRecorder(stream, {
mimeTyp: 'audio/mp3',
});
// 开始录音
mediaRecorder.current.onstart = function (e) {
recordTime = 0;
handleRecordTime('start');
};
// 录音暂停
mediaRecorder.current.onpause = function (e) {
handleRecordTime('stop');
};
// 录音继续
mediaRecorder.current.onresume = function (e) {
handleRecordTime('start');
dataReset();
};
// 录音结束
mediaRecorder.current.onstop = function (e) {
handleRecordTime('stop');
onUploadSuccess(dataBuffer);
// 增加延时,防止 onUploadSuccess 有异步操作
setTimeout(() => {
dataReset();
})
};
// 录音错误
mediaRecorder.current.onerror = function (e) {
handleRecordTime('stop');
};
// 录制的资源,录音结束才会触发
mediaRecorder.current.ondataavailable = function (e) {
dataBuffer.push(e.data)
};
// 进入页面,录音器加载完后就开始录音
if (isStartRecord) {
onStart();
isStartRecord = false;
}
})
.catch((err) => {
if (
err.toString().includes('denied') &&
(err.toString().includes('Permission') || err.toString().includes('permission'))
) {
alert('录音授权失败,请清除缓存后再操作');
return;
}
alert(err);
});
};
// 开始录音
const onStart = async () => {
// state: inactive -未开始, recording - 录音中,paused - 录音暂停
if (mediaRecorder?.current?.state === 'inactive') mediaRecorder.current?.start();
};
// 结束录音
const onStop = () => {
if (mediaRecorder.current && mediaRecorder.current?.state !== 'inactive') mediaRecorder.current.stop();
};
// 继续录音
// isload 页面初次加载完成后开始录音
const onContinue = async (isload) => {
if (mediaRecorder?.current?.state === 'paused' && recordTime) mediaRecorder.current.resume();
else if (!mediaRecorder?.current && isload && !recordTime) isStartRecord = true;
else if (!recordTime) onStart();
};
// 暂停录音
const onPause = () => {
if (mediaRecorder?.current?.state === 'recording') mediaRecorder.current.pause();
};
// 录音兼容问题处理
const initUserMedia = () => {
// eslint-disable-next-line no-undef
if (navigator.mediaDevices === undefined) {
// eslint-disable-next-line no-undef
navigator.mediaDevices = {};
}
// eslint-disable-next-line no-undef
if (navigator.mediaDevices.getUserMedia === undefined) {
// eslint-disable-next-line no-undef
navigator.mediaDevices.getUserMedia = function (constraints) {
// eslint-disable-next-line no-undef
const getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
if (!getUserMedia) {
return Promise.reject(new Error('浏览器不支持 getUserMedia !'));
}
return new Promise((resolve, reject) => {
// eslint-disable-next-line no-undef
getUserMedia.call(navigator, constraints, resolve, reject);
});
};
}
};
/**
* init
*/
const initRecord = async () => {
await initUserMedia();
initMediaRecorder();
};
useEffect(() => {
initRecord();
}, []);
return {
onContinue,
onStart,
onStop,
onPause,
};
};
export default useRecord;
注意事项
- 录音的 H5 网络协议得是
https
协议 - 录音最好设置最大时长;时长过大,录音文件大小会比较大,上传速度会有影响
- 嵌入到小程序中,需配置
webview
域名 - 与
webview
进行通信,在onMessage
获取数据;返回的数据是多次postMessage
的数据组成的数组。 - 在 H5 中通过
wxjsddk
进行操作,postMessage
进行数据通信,只会在小程序后退、组件销毁、分享、复制链接后触发。 - 更多请查看API:developers.weixin.qq.com/miniprogram…