文章目录
录音、上传、播放音频微信小程序实践实践分析依赖接口录音上传播放Page 事件参考录音、上传、播放音频微信小程序实践
最近上线了一款智能外呼机器人产品,需要开发一款录音、上传、播放音频功能的
微信小程序给录音师配置外呼话术真人录音。
代码已开源,数据均已本地化处理。适合新手参考学习的完整原生微信小程序小项目。
实践分析
依赖接口
主要使用以下 api
wx.getRecorderManager :获取全局唯一的录音管理器 RecorderManagerwx.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, // 最多录音时长 10 分钟sampleRate: 8000, // 采样率numberOfChannels: 1, // 1 个录音通道即可format: 'wav', // 服务端指定格式};recorderManager.start(recordOptions)
初始状态 录音检测是否收到声音,本想利用RecorderManager.onFrameRecorded
来感知是否收到声音,
展示波形图,但该事件不支持wav
格式文件。目前监听到开始事件即显示录音计时。
// 监听已录制完指定帧大小的文件事件。如果设置了 frameSize,则会回调此事件。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),});// await this.getDetail('CUR');});
监听录音异常、中断,录音异常千奇百怪,且无文档具体说明。
比如电话会打断录音,触发暂停事件。拒绝授权会出发错误事件。这里都设置异常变量为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;// 获取 CDN tokengetNosToken({fileName: name }).then((data) => {console.log('uploadToken: ', data);wx.uploadFile({url: '/',name: 'file', // 服务器获取流的参数名filePath,formData: {Object: data.objectName,'x-nos-token': data.token,},success(res) {console.log('上传成功回调', res);wx.hideLoading();const url = `/${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) => {/*** 10001系统错误* 10002网络错误* 10003文件错误* 10004格式错误* -1 未知错误*/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();},