录音、上传、播放音频微信小程序实践
最近上线了一款智能外呼机器人产品,需要开发一款录音、上传、播放音频功能的 微信小程序给录音师配置外呼话术真人录音。
代码已开源,数据均已本地化处理。适合新手参考学习的完整原生微信小程序小项目。
实践分析
依赖接口
主要使用以下 api
- wx.getRecorderManager :获取全局唯一的录音管理器 RecorderManager
- wx.createInnerAudioContext : 创建内部 audio 上下文 InnerAudioContext 对象
PS.
- 默认
audio 组件样式不符合需求,目前只需播放进度条,InnerAudioContext 配合 process 组件实现 InnerAudioContext 退出小程序自动停止播放,需要退出小程序依然可播放请使用背景音频 BackgroundAudioManager 代替
为什么要声明全局变量:
- 录音本身就是唯一全局
- 语音播放,如果每次离开、进入页面动态生成、销毁(好像有 bug),会有多条音频同时播放,为避免这个问题,使用全局唯一对象管理
const recorderManager: WechatMiniprogram.RecorderManager = wx.getRecorderManager();
const innerAudioContext: WechatMiniprogram.InnerAudioContext = wx.createInnerAudioContext();
录音
const recordOptions = {
duration: 10 * 60 * 1000,
sampleRate: 8000,
numberOfChannels: 1,
format: 'wav',
};
recorderManager.start(recordOptions)
- 录音检测是否收到声音,本想利用
RecorderManager.onFrameRecorded 来感知是否收到声音, 展示波形图,但该事件不支持 wav 格式文件。目前监听到开始事件即显示录音计时。
recorderManager.onFrameRecorded(({ frameBuffer, isLastFrame }) => {
console.log('frameBuffer.byteLength: ', frameBuffer.byteLength)
console.log('isLastFrame: ', isLastFrame);
});
- 监听录音开始事件,设置录音进行中状态,并展示录音计时器
recorderManager.onStart(() => {
console.log('recorder start');
this.startClock();
this.setData({
...recordingData,
});
});
- 停止录音事件,可以接收到本地录音文件地址、录音时长信息。一般上传文件至
CDN ,然后把地址存储到业务服务器,接着试听播放。
recorderManager.stop();
recorderManager.onStop(async (res) => {
console.log('recorder stop', res)
this.stopClock();
this.setData({
...initRData,
});
if (isError) {
isError = false;
return;
}
const { tempFilePath, duration } = res;
console.log('tempFilePath', tempFilePath);
const url = await uploadFile({ filePath: tempFilePath });
if (innerAudioContext.currentTime) {
innerAudioContext.stop();
}
innerAudioContext.src = url;
this.setData({
...initPlayData,
...initRData,
detail: {
...this.data.detail,
url,
duration: Math.ceil(duration / 1000),
},
duration: formatClock(duration, true),
});
});
- 监听录音异常、中断,录音异常千奇百怪,且无文档具体说明。
比如电话会打断录音,触发暂停事件。拒绝授权会出发错误事件。这里都设置异常变量为 true ,在 onStop 事件中不进行上传逻辑,而是恢复到录音初始状态。
recorderManager.onError((err) => {
this.noEffectStopRecorder();
showErrMsg(msgMap[err.errMsg] || err.errMsg || '小程序错误');
console.log('recorderManager.onError', err);
});
recorderManager.onPause(() => {
console.log('recorder pause');
this.noEffectStopRecorder();
});
- 记录异常不进行业务处理并调用终止录音。这里注意录音不像播放调用
stop 是无副作用的。未开始或暂停录音调用 stop 会抛出异常。小心导致死循环。
noEffectStopRecorder() {
if (this.data.isRecording) {
isError = true;
recorderManager.stop();
}
}
上传
export function uploadFile({ fileName, filePath }: {
fileName?: string;
filePath: string;
}) {
return new Promise<string>((resolve) => {
wx.showLoading({
title: '上传中...',
});
const name = fileName || filePath;
getNosToken({ fileName: name }).then((data) => {
console.log('uploadToken: ', data);
wx.uploadFile({
url: 'https://nos.com/',
name: 'file',
filePath,
formData: {
Object: data.objectName,
'x-nos-token': data.token,
},
success(res) {
console.log('上传成功回调', res);
wx.hideLoading();
const url = `https://cdn.com/${data.objectName}`
console.log(url);
resolve(url);
},
fail(err) {
wx.hideLoading();
wx.showToast({
title: err.errMsg,
icon: 'none',
});
reject(err);
},
})
});
});
}
播放
innerAudioContext.onPlay(() => {
console.log('开始播放');
this.setData({
...playingData,
});
});
- 监听音频播放进度更新事件,更新
process 百分比
innerAudioContext.onTimeUpdate(() => {
console.log('监听音频播放进度更新事件');
let playPercent = 0;
const duration = this.data.detail.duration || innerAudioContext.duration;
try {
playPercent = Math.ceil(((innerAudioContext.currentTime * 1000) / (duration * 1000)) * 100) || 0;
} catch (e) {
playPercent = 0;
}
playPercent = playPercent && playPercent > 100 ? 100 : playPercent;
const currentTime = formatClock(innerAudioContext.currentTime * 1000, true);
console.log('当前播放时间:', currentTime);
console.log('微信暴露时间:', innerAudioContext.duration);
console.log('后端返回时间:', duration);
console.log('当前播放进度:', playPercent);
this.setData({
currentTime,
playPercent,
});
});
- 需求不需要暂停或拖拽进度条。监听音频正常、异常停止或暂停时,都恢复到初始状态。需要恢复或拖拽进度能力,可自行相应事件中处理
innerAudioContext.onEnded(() => {
console.log('监听音频自然播放至结束的事件');
this.setData({
...initPlayData
});
});
innerAudioContext.onError((res) => {
console.log(res.errCode, res.errMsg);
this.setData({
...initPlayData
});
});
innerAudioContext.onPause(() => {
console.log('监听音频暂停事件');
this.setData({
...initPlayData
});
});
innerAudioContext.onStop(() => {
console.log('监听音频停止事件');
this.setData({
...initPlayData,
});
});
Page 事件
- 页面初次渲染完成,初始化音频录音、播放事件
- 页面每次重新进入加载最新业务数据
- 页面离开当前页面或退出小程序,停止录音、播放
onReady() {
this.initRecorder();
this.initAudioPlayer();
},
onShow() {
this.getDetail('CUR');
},
onUnload() {
console.log('切换页面停止录音或播放');
innerAudioContext.stop();
this.noEffectStopRecorder();
},
参考
- 原文地址
- Github 地址
|