本文介绍小程序安卓手机播放语音文件错误问题的分析过程与解决方案,该问题出现较多,问题隐藏较深,按本文方案可以解决该问题。
一、问题现象
微信小程序已经放弃了基于wx.createAudioContext()的audio组件,要求开发人员使用功能更强大的wx.createInnerAudioContext()组件,新的给件功能的确要强大得多,不需要在页面上布置组件,旧的组件只有play、pause、seek三个事件,功能是比较弱,新的Inner组件增加了很多事件,并且还有监控事件,可以自己定义UI界面,通过监控事件可以在准备好播放、播放完成等,增加用户函数,可以统计用户使用情况,可以进行很多操作,如插播内容、转换等。
但是,将组件按为新的wx.createInnerAudioContext()后遇到了一个大坑,即部分安卓手机播放时后端报错,无法播放语音文件。在电脑上调试、浏览器上访问、苹果手机上都可以,就是安装手机网上提这个问题的人很多,但都没有提供完整的解决方案,微信官方也没有提供解决方案,只是让大家提交代码片断,只说是与机型不兼容。
真机调试时,后台错误代码:
errCode: 10001, errMsg: “errCode:62, err:load or init native decode so fail”。
同时音频对象监听错误的事件,也能监听到错误,错误信息:
监听音频播放错误事件 {errCode: 0, errMsg: “”}
为了项目进展,没有办法,先退回去用旧的wx.createAudioContext()组件,这个组件虽然功能差一些,但基本上能用,先凑合作。后来,有时间后进一步在研究了手机兼容问题、音频文件服务Header、保存文件OSS问题、音频文件内部格式问题后,终于找到了解决方案,特此完整记录一下,希望对遇到类似问题的同行有用。
二、原因分析
刚碰到这个问题也是一头雾水,百度上能找到的相关信息很少,微信小程序社区提这个问题的人很多,将这个错误代码在社区上检索,有7000多条信息,但都是反馈问题的,没有找到可用的解决方案,不过线索找到了几条。
2.1 检测组件使用方法
刚开始怀疑程序编写方法不对,而微信小程序的官方手册介绍内容很少,还不容易理解,官方手册将对象申明放到头上,并且按const申明,并且只有一个SRC,我的项目中要用到多个动态SRC,并且SRC是用户选择不同景点,播放不同的语音介绍。
const innerAudioContext = wx.createInnerAudioContext();
刚开始,是怀疑此问题,后专门写了一个页面,按照官方的写法,结果一样,仍然不行。后来反复尝试写各种方案,将对象放到this,this.data仍然不行,测试过将对象所有按钮事件,监控事件都写了来跟踪各种情况 ,结果仍是一样,在测试平台、ios真机测试行,安卓真机测试不行。
2.2 文件请求header问题排除
先退回旧组件继续使用,旧的组件需要在页面文件添加组件,为了在实现多个对象用一个后台组件自己定义了play和pause图标,定认了一个audio组,并隐藏起来,实现与inner对象差不多的效果,但控制起来比较麻烦,且没有监控事件,后续做播放统计会有一些问题。
不甘心退回用旧组件,一有时间继续研究该问题,在微信社区发现讨论此问题的很多,但没有明确答案,基本上说是不兼容,问题原因是微信小程序的组件不兼容,只能等微信升级,但是这个问题从2019年3月就广泛出现了啊,一年多了微信还没升级吗。后来找到一个兄弟说解决了是request heder问题,修改请求的heaer即可,但没有给出方法啊,也没有进一步回复。
反复研究request header,在我用的VUE的request确实是判断header,后台header需要调协为octect-stream,但是我后台已经是mp3文件了啊,不论用新旧组件,都只有一个src参数,指向后端地址,没有地方加header啊,除非后台将文件读取到内存,然后再以stream反馈给前端,但是觉得使用OSS就是让前端可以直接通过链接方式,以流方式使用多媒体文件啊,如果再读回服务器内存,再反馈给前端不对啊,应该没有必要。
if (headers['content-type'] === 'application/octet-stream;charset=UTF-8') {
return res.data
}
继续研究,如果使用OSS或者其它服务器用链接访问,都无法增加header的请求头,解决这个方案的兄弟应该是以流方式提供给前端小程序的,不是以后端OSS对象文件服务提供的小程序的。
2.3 文件服务器问题分析
继续下载微信社区上相关的代码片段,发现一个有意思的问题,有iac应用一个代码片段,第一次播放报错,等待一会儿,再点击播放按钮时,就不会报错了。觉得这个问题是否应该是服务器与小程序的冲突所至,文件前端还没有获取完成,前端就播放了,将自动播放去掉,修改为自播放,或者在onCanPlay事件中再播放,还有错,问题没有得到解决。想到自己用的OSS服务器是阿里,小程序是腾讯,是不是这两个公司互相限制造成的问题。沿着这个思路,检查文件服务位置进行测试,果然将其它服务器基本上行,自己在阿里OSS上的不行,以为是阿里和腾讯互相限制的结果,但想想不会啊,计算机上和苹果计算机上行的啊。于是可以播放的文件下载下来,上传到自己的OSS上进行测试,竟然能够播放,于是想明白了,是不是文件本身有问题啊。至此,排除了小程序写法、请求头参数、文件服务器问题,此问题应该是文件本身问题,于此顺着此思路,还真找到了相关资料,解决了此问题。
三、MP3文件标签分析
由于我是自己学习,前后端都比较了解,顺着MP3文件格式问题思路继续,查找,果然在微信小程序开发社区找到了下面文章,该文章介绍引用了MP3文件格式标记问题,并且提到了讯飞语音合成的mp3不能播放,转百度语音合成就以。并在这个问题的回复文间中,介绍了一个给mp3文件添加标记的程序,这两个方案结合在一起给了解决问题的思路与方法。
https://developers.weixin.qq.com/community/develop/article/doc/000460e9bd4c982e1609f4f725b013
3.1 MP3文件格式解析
MP3 文件大体分为三部分:TAG_V2(ID3V2),音频数据,TAG_V1(ID3V1),详细的见下面文章,不再赘述。
https://blog.csdn.net/datamining2005/article/details/78954367
3.2 讯飞语音合成MP3文件格式
根据3.1介绍的内容,下载格式工厂,对讯飞语音合成文件信息进行解析,果然只有文件size,其它信息都没有,如下图。
另外找了下MP3音乐文件解析如下,有详细的信息。
用十六进制文件找开,头上没有id3,后面没有tag
至此,可以判断是讯飞合成的语音文件是使用lame3.1格式对音频进行编码,然后以扩展名MP3进行识别,在游览器、微信小程序调试器、苹果真机调试这两个信息足够解析并播放,但安卓版微信小程序是标签进行识别的,无法正常播放。
对此,对讯飞合成用格式工厂进行转换,然后上传到阿里OSS文件上,测试能够正常播放。至此,问题的原因定位清楚:
1、讯飞语音合成的MP3文件只是用lame3.1格式进行编码,并以mp3扩展名进行标识识别,并没用在文件中添加MP3音频文件前后增加TAG_V2(ID3V2)、TAG_V1(ID3V1)标记。
2、微信小程序安卓上的音频播放器有BUG,是按音频文件内部的TAG来识别和解析文件,对于TAG不正确的,不能播放,但这个问题在计算机浏览器和苹果计算机上存在,只存在安卓系统,因此前端工程师很难解决这个问题,导致微信社区上千个这样的问题没有完整的解决方案回复。特别是,很多偶然现现的问题,原因是文件的问题,不是不兼容问题。
四、修复方案
找到根本原因后,参照下面方案进行修复,分析讯飞语音合成文件的资料和参数,没有找到添加TAG的方法,没办法自己添加吧,感谢两位同行兄弟提供的思路。
https://www.cnblogs.com/ztysir/p/5513853.html
4.1 语音文件添加TAG方法
添加两个方法,分别根据传入的音频文件名称,作者,专辑名,添加到头和尾部,这两个方法基本上借用上面兄弟的代码。
/**
* * 合成mp3文件的tag
* *
* * @songName 名称
* * @artistName 作者
* * @albumName 专辑
* * @return 128字节的D3V1字符串
*
*
* @return
*/
private static byte[] composeD3V1(String songName, String artistName,
String albumName) {
try {
byte[] tagByteArray = new byte[128];
byte[] songNameByteArray = songName.getBytes("GBK");
byte[] artistNameByteArray = artistName.getBytes("GBK");
byte[] albumNameByteArray = albumName.getBytes("GBK");
int songNameByteArrayLength = songNameByteArray.length;
int artistNameByteArrayLength = artistNameByteArray.length;
int albumNameByteArrayLength = albumNameByteArray.length;
songNameByteArrayLength = songNameByteArrayLength > 30 ? 30 : songNameByteArrayLength;
artistNameByteArrayLength =
artistNameByteArrayLength > 30 ? 30 : artistNameByteArrayLength;
albumNameByteArrayLength =
albumNameByteArrayLength > 30 ? 30 : albumNameByteArrayLength;
System.arraycopy("TAG".getBytes(), 0, tagByteArray, 0, 3);
System.arraycopy(songNameByteArray, 0, tagByteArray, 3, songNameByteArrayLength);
System.arraycopy(artistNameByteArray, 0, tagByteArray, 33, artistNameByteArrayLength);
System.arraycopy(albumNameByteArray, 0, tagByteArray, 63, albumNameByteArrayLength);
// 将流派显示为指定音乐的流派
tagByteArray[127] = (byte) 0xFF;
return tagByteArray;
} catch (Exception e) {
log.