文章首发:掘金主页
一、前言
- 开发背景:首次尝试小程序中实现录音、播放功能。
- 开发框架:
- taro 2.2.6
- taro-ui 2.3.4
- 难点描述:
- 实现小程序录音、上传到后台
- PC、IOS 和安卓端音频播放资源的地址,支持 mp3 下载链接
温馨提示:这篇文章重点介绍小程序的音频在各种环境录音和播放实践。适用对象:遇到小程序在 IOS 端无法播放音频的同学们和对小程序兼容性感兴趣的同学。
二、小程序录音、上传
2.1 注册事件监听
首先,介绍一下录音的部分。这里主要用到了小程序中的 wx.getRecorderManager()
模块部分。
直接放代码,感兴趣的可以去微信开发文档就了解下各种配置。
import Taro, { Component } from '@tarojs/taro'
export default class Index extends Component {
...
// 声明录音管理器模块
recorderManager = wx.getRecorderManager()
componentDidMount() {
// 抛出错误
recorderManager.onError(() => {
Taro.showToast({
title: '录音失败!',
duration: 1000,
icon: 'none'
})
})
// 录音结束时的处理
recorderManager.onStop(res => {
if (res.duration < 1000) {
Taro.showToast({
title: '录音时间太短',
duration: 1000,
icon: 'none'
})
} else {
// content 是存储录音结束后的数据结构,用于调试
this.setState({ content: res })
wx.saveFile({
tempFilePath: res.tempFilePath,
success: result => {
// 这里会调用一个文件上传的接口
this.fileUpload(result.savedFilePath)
}
})
}
})
}
fileUpload(tempFilePath) {
Taro.uploadFile({
url: XXXApi,
filePath: tempFilePath,
name: 'file',
header: {
'content-type': 'multipart/form-data',
cookie: Taro.getStorageSync('cookie') // 上传需要单独处理 cookie
},
formData: {
method: 'POST' // 请求方式
},
success: res => {
// 录音上传成功之后的处理
}
})
}
}
梳理一下:
- 在
componentDidMount
生命周期中,注册几个重要的事件。包括:监听录音错误事件和监听录音结束事件 - 在录音结束时,用
wx.savefile
将文件保存到本地 - 在
wx.savefile
成功的回调中,调用文件上传的接口,将文件上传到服务器。
2.2 实现录音事件处理函数
先看下 dom
节点部分:
<Text>上传语音</Text>
<Text
onLongPress={this.handleRecordStart}
onTouchend={this.handleRecordStop}
>
长按说话
</Text>
其中就两个事件:handleRecordStart
和 handleRecordStop
。他们分别是长按时触发和手指松开时触发。
简单实现:
// longpress (长按)时触发
handleRecordStart(e) {
this.setState({
record: {
// 修改录音数据结构,此时录音按钮样式会发生变化。
text: '松开保存',
type: 'recording'
}
})
// 开始录音
this.recorderManager.start({
duration: 60000,
sampleRate: 44100,
numberOfChannels: 1,
encodeBitRate: 192000,
format: 'mp3',
frameSize: 50
})
Taro.showToast({
title: '正在录音',
duration: 60000,
icon: 'none'
})
}
// touchend (手指松开)时触发
handleRecordStop() {
// 复原在 start 方法中修改的录音的数据结构
this.setState({
record: {
text: '长按录音',
type: 'record'
}
})
// 结束录音、隐藏 Toast 提示框
wx.hideToast()
// 结束录音
this.recorderManager.stop()
}
这里用了一个 record
对象来记录录音的状态。
注意 recorderManager.start
方法的参数中, duration
指录音时长,这里设置为 60000 ms
;format
值为 mp3
,意思录音得到的音频文件为 mp3
格式。
温馨提示:最初开发没有设置成格式化为
mp3
,导致后台同事增加了工作量(将m4a
转换成mp3
),这里建议前端直接处理,很方便。
三、小程序端录音的播放
3.1 录音播放
说到音频播放,大家第一时间可能想到的是 Audio
标签,然后给其中的 src
属性动态赋值就好了。没错,PC 端确实是这样。但是小程序比较坑,如下图:
音频播放这里,我们选用了 wx.createInnerAudioContext()
接口。
温馨提示:如果音频上传到后台之后可以返回
.mp3
结尾的url
链接(例如:http://47.104.167.164/faceVideo/result_2020_07_21_12_33_43.mp3
),可以考虑直接利用wx.createInnerAudioContext()
的play()
方法实现播放。
由于部分原因,我们后台上传音频文件后,返回的链接是一个云文件 ID(指浏览器打开可以下载此 mp3 文件)。而且经过测试发现,安卓端可以直接播放,IOS 端直接播放没有声音。
然后,请教了一下我们组的架构师,决定将文件先下载下来,然后保存到手机本地,最后播放(经过测试方案可行)。
我们直接看代码:
// 小程序音频播放 api
innerAudioContext = wx.createInnerAudioContext()
// 下载音频文件
downloadFile() {
const FileSystemManager = wx.getFileSystemManager()
const { voiceUrl } = this.state
wx.downloadFile({
url: voiceUrl,
header: { 'Content-type': 'audio/mp3' },
success: res => {
// 只要服务器有响应数据,就会把响应内容写入文件并进入 success 回调,业务需要自行判断是否下载到了想要的内容
if (res.statusCode === 200) {
FileSystemManager.saveFile({
tempFilePath: res.tempFilePath,
// 文件地址为手机本地
filePath: `${wx.env.USER_DATA_PATH}/${new Date().getTime()}.mp3`,
success: result => {
if (result.errMsg == 'saveFile:ok') {
this.registerAudioContext(result.savedFilePath)
}
}
})
}
}
})
}
// 注册音频控件
registerAudioContext(path) {
this.innerAudioContext.src = path
this.innerAudioContext.play()
// 避开 IOS 端静音状态没法播放的问题
this.innerAudioContext.obeyMuteSwitch = false
this.innerAudioContext.onEnded(res => {
// isPlaying 记录是否在播放中
this.setState({ isPlaying: false })
this.innerAudioContext.stop()
})
this.innerAudioContext.onError(res => {
// 播放音频失败的回调
})
this.innerAudioContext.onPlay(res => {
// 开始播放音频的回调
})
this.innerAudioContext.onStop(res => {
// 播放音频停止的回调
})
}
这里做了两件事情:
- 用
wx.downloadFile()
接口将文件下载下来,注意参数中header
属性,Content-type
值为audio/mp3
。即将此文件识别为音频类文件。这里用到微信里的文件管理器wx.getFileSystemManager()
,接口中的saveFile()
方法可以把文件保存到本地 - 用
wx.createInnerAudioContext()
的play()
方法播放存在本地的音乐mp3
文件
3.2 性能优化
这里考虑到播放完之后,存在手机的录音文件会越来越多。我们想想办法,做一做性能优化工作。也就是在恰当的时机清楚多余文件。
代码如下:
componentWillUnmount() {
this.clearDir()
}
// 删除下载的音频文件
clearDir() {
const FileSystemManager = wx.getFileSystemManager()
const __dirPath = wx.env.USER_DATA_PATH
FileSystemManager.readdir({
dirPath: __dirPath,
success: res => {
const { errMsg, files } = res
if (errMsg == 'readdir:ok') {
files.forEach(item => {
FileSystemManager.unlink({
filePath: `${__dirPath}/${item}`
})
})
}
}
})
}
梳理一下:
用 wx.getFileSystemManager()
接口中 readdir()
方法读取到指定目录(wx.env.USER_DATA_PATH
)的所有文件。在其读取成功的回调中做一个 forEach
循环,然后用 unlink()
删除文件。最后将此方法放在生命周期 componentWillUnmount
中调用。
四、PC 端音频播放
小程序的录音和播放都简单的介绍了,这里也拓展一下。说一说 PC 端比较原始的音频播放方法。
项目中没有引用播放器插件,这里直接用 audio
标签来实现。 html 的部分如下:
const { voice_url, isPlaying } = this.state;
return (
<>
<p>
<span>音频:</span>
<Button onClick={this.onBtnClick}>{isPlaying ? '停止' : '播放'}</Button>
</p>
<audio
id={`audio`}
src={voice_url}
autoPlay={true}
ref={this.audioRef}
preload={'auto'}
onCanPlay={() => {}}
onTimeUpdate={() => {}}>
<track src={voice_url} kind='captions' />
</audio>
</>
)
然后看下 PC 端解析播放部分,和小程序原理差不多,先下载,后播放。代码如下:
// 播放或者暂停
onBtnClick = () => {
const { isPlaying } = this.state;
// 区分播放还是暂停
if (isPlaying) {
this.audioRef.current.pause();
} else {
this.downloadFile();
}
this.setState({ isPlaying: !isPlaying });
};
// 下载文件
downloadFile = () => {
const { download_url } = this.state;
axios.get(download_url as string, { responseType: 'blob' }).then((res: any) => {
const reader = new FileReader();
const data = res.data;
reader.onload = e => {
this.executeDownload(data);
};
reader.readAsText(data);
});
};
// 在浏览器上预览音频文件
executeDownload = (data: any) => {
if (!data) {
return;
}
// 将文件转化音频流的链接
const url = window.URL.createObjectURL(new Blob([data], { type: 'audio/mp3' }));
// 前端存储这个链接
this.setState({ voice_url: url });
};
梳理:
- 创建
audio
标签作为音频播放的容器 - 点击页面的播放按钮触发文件下载方法
- 通过
axios
下载资源文件,用new FileReader()
读取文件,并且在文件完全加载时,利用window.URL.createObjectURL()
方法生成可以在浏览器上预览音频文件的链接 -
audio
监听到src
属性的变化时,会自动播放出声音
五、感谢
- 如果本文对你有帮助,就点个赞支持下吧!感谢阅读。
- 文中还有很多不完善的地方,欢迎大家在评论区提出疑问。