前言
最近受人所托,一直在研究关于移动端<audio>标签相关的兼容性问题,对<audio>标签有了新的认识,甚至还涉猎些许web-audio-api(一个很好玩的东西本文不展开讲述)。接下来就探讨一下本次经历过程,希望能对同样遭遇的小伙伴有些许帮助~
特别提醒
关于<audio>标签移动端的兼容问题,并不是单纯靠前端的调试就能解决的,需要后端同步进行调测!
一、第一次调试
关于audio标签的问题,本人也上网查了不少的资料,包括stackoverflow在内的好多社区(当然包括掘金啦),甚至查询到了chrome早期版本<audio>的currentTime属性必须设置为字符串格式。奇怪的知识又增加了!
首先项目在vue上,于是乎自己就撸了一个简单的audio组件,功能尚不完善,作为测试用途,当然大家也可以参考。
github地址:github.com/Chrischenny…
当然也在npm上面发布了~也可以供下载使用~
npm install ch-audio
好了接下来进入正题!
问题症结所在: 自定义的audio组件,进度条无法拖拽,或者说拖拽后回到起点。
首先来看一下我们组件拖拽部分的逻辑代码:
data(){
return{
url:'',
stop:true,//暂停标志
progressX:0,//进度条宽度数值,缓存用
offset:0,//进度条与屏幕的距离
audioitself:null,//缓存audio
block:null//缓存触摸区域
}
},
methods:{
playAudio(){ //播放
this.stop = !this.stop;
this.$refs.oad.play();
},
stopAudio(){ //暂停
this.stop = !this.stop;
this.$refs.oad.pause();
},
handleTouchstart(e){//触摸事件开始事件
//移动开始,移除timeupdate事件,防止对进度条产生干扰;
this.audioitself.removeEventListener('timeupdate',this.handleTimeupdate);
this.progressX = e.changedTouches[0].clientX - this.offset;
},
handleTouchmove(e){//触摸移动侦听事件
if(e.changedTouches[0].clientX - this.offset>this.width){
this.progressX = this.width;
}else if(e.changedTouches[0].clientX - this.offset<0){
this.progressX = 0;
}else{
this.progressX = e.changedTouches[0].clientX - this.offset;
}
},
handleTouchend(e){//触摸结束侦听事件
this.audioitself.currentTime = ((e.changedTouches[0].clientX-this.offset)/this.width)*this.audioitself.duration;
//触摸事件结束,重新添加timeupdate事件
this.audioitself.addEventListener('timeupdate',this.handleTimeupdate);
},
handleTimeupdate(){//播放进度侦听事件
this.progressX = this.width*(this.audioitself.currentTime/this.audioitself.duration)
},
}
逻辑不多说了,基本进度条都是这样设置的(vue因为数据的双向绑定,设置起来可能更加方便),说一下遇到的问题,我们观察问题的时候一定要细致,而不是简单的就表面的问题就暂停了:1、本地调测没有发现任何问题,于是代码丢到服务器上,手机端测试,发现进度条不是无法拖拽,而是不松手,进度条是可以跟着手走的,在松手的那一刻,进度条回到了原点。 这说明问题出在了touchend事件上
二、第二次调试
结合代码想当然的觉得问题就出现在了currentTime的设置上,于是就有了上面一出chrome的历史遗留问题。进行currentTime调试时新的问题出现,浏览器端的进度条在拖拽之后也会回到原点。。。奇怪!(此处我用的是服务器上的页面)。打印了一下currentTime:正常
之前本地调测的时候,没有这问题,而将页面放到服务端的时候却出现了问题!那么,问题是不是就出在服务端呢?
在我辗转稳了多方大佬之后,有了答案:原来我们需要在服务端设置一个响应头:
"Accept-Ranges":"bytes"
没错,只有设置了这个响应头,就能正确拖动进度条,那么为什么呢?知其然,不知其所以然,那和不知有什么区别! 继续查资料!
首先来看一下未设置该响应头的情况,我们的请求头和相应头是怎么样的。

"Accept-Ranges":"bytes"到底是个什么!
响应头Accept-Ranges:
我们来看看MDN对于这个请求的说法:
当浏览器发现"Accept-Ranges"头时,可以尝试继续中断了的下载,而不是重新开始。
接下来我们看调试界面:

"Accept-Ranges"所以浏览器并没有继续下载,如果你不点击播放,浏览器也就不会再向服务器去请求数据。
也就是说,如果我们我们没有正确的设置"Accept-Ranges",浏览器会认为我们将重新开始下载该视频,而不是从断点处继续进行下载,自然也不存在缓存之类的说法,所以浏览器就会禁止我们人为的设置currentTime(虽然点击播放键后,开始下载视频了,但是响应头有keepalive,所以浏览器认为是一次链接,所以currentTime仍然不能设置,其实哪怕超时断开连接,只要你的响应头没有"Accept-Ranges":"bytes"还是会不让你设置的,貌似IE11可以设置了,有大佬愿意去试试么,嘿嘿)
这里贴两篇参考资料,不一定是最好的,但是对我很有帮助!
三、第三次调试
有了上述的结论,喜出望外!是不是成功了呢!开始手机调试,安卓成功!IOS还是失败!
快要崩溃了,为什么IOS还是失败= =;IOS可真是移动端兼容的噩梦!继续进行调试吧,接下来的调试全部需要在手机端进行了,好不方便。。。
问题代码区域:
handleTouchend(e){//触摸结束侦听事件
this.audioitself.currentTime = ((e.changedTouches[0].clientX-this.offset)/this.width)*this.audioitself.duration;
//触摸事件结束,重新添加timeupdate事件
this.audioitself.addEventListener('timeupdate',this.handleTimeupdate);
},
1、首先检查currentTime,出现问题,显示NaN!既然是NaN,那么肯定是数字计算的某个值出现了问题!
2、首先e.changedTouches[0].clientX(天晓得ios监听事件会搞出什么幺蛾子也检查一下),没问题!
3、其次this.offset,this.width都是数字没问题。
4、最后,this.audioitself.duration,出问题了,显示的是infinity!!
为什么会是infinity?
首先想到的是,那么我们的<audio>是如何获取到自己的duration的?答案很明显,就是从服务端获取!我们再来看一下服务端的返回的头,这次是加上了"Accept-Ranges":"bytes"的响应头。

Content-Length:****这个字段就是表示文件的大小的,也是<audio>计算出自身duration根本所在。但是我服务器是不分什么终端,返回的响应头都是这个格式,为什么你IOS就这么傲娇不认我这个头呢??
进过之后的调试,发现IOS和安卓在请求头上的区别,安卓的请求头字段是这样的:"Range":"bytes=0-",这个什么意思,直白的翻译:“我要问你要文件,从第0bytes开始要,给多少么,看你的心情。”那服务器不就大大方方把数据给你了么。
IOS的Range请求头
但是IOS系统傲娇,从服务器调试可以看出,IOS先给我发了一个"Range":"bytes=0-1"请求。。。你也太小家子气了吧,就要1byte??接下来才是最气的你如果按照Android的方式返回请求头,人家根本不care你了,也不会再来问你要数据字节了,自然你的duration就变成infinity了!见下图请求头:

不卖关子,就是Content-Range头。我们来看看MDN对于Content-Range的定义:
在HTTP协议中,响应首部 Content-Range 显示的是一个数据片段在整个文件中的位置。
感觉像是没听懂的亚子,看一下格式:
Content-Range: <unit> <range-start>-<range-end>/<size>
Content-Range: <unit> <range-start>-<range-end>/*
Content-Range: <unit> */<size>
那么,IOS如果需要获取音频大小的信息,但是它不认Content-Length头(当然也可能是拿这两个字段进行校验??),那么自然只能从Content-Range中来了。。。(= =;真奇怪,人家明面上告诉你的 你不要,非要自己再去解析)而上述三个格式只有第一个是带文件大小的,那么就很明确了。所以后端的接口就不能是直接静态来访问了,而是要动态的相应IOS的需求,并返回相应的响应头了,可以参考以下代码,用koa框架:
router.get('/getsource/:filename',async ctx=>{
var mp3 = path.resolve('./view/source/' + ctx.params.filename);//获取服务器音频资源的具体路径
ctx.set({
'Content-Type': 'audio/mpeg',
'Content-Length': fs.statSync(mp3).size,//如果不是静态获取,这两个头需要自己加上,且必须加上
})
if (ctx.headers.range === 'bytes=0-1') {//判断是不是IOS发起的预请求
ctx.set('Content-Range', `bytes 0-1/${fs.statSync(mp3).size}`) // 重点在这
ctx.body = '1'
} else {
ctx.set({
'Accept-Ranges': 'bytes',
})
const src = fs.createReadStream(mp3)
ctx.body = src
}
});
到了这里基本大功告成了!测试一下,可以拖拽,一切正常了! 就是本小白的服务器有点慢。。加载需要时间 。。。 然后,最后我们再看看,我们正确响应了IOS的请求后,IOS后续会给我们发来什么请求:


可以看到,IOS的第二次请求仍然是小额的字节就只要16000多bytes,但是第三次请求就一次性全部拿走了150万字节。
而且,我还发现每次重新测试IOS第二次要的字节数都是同一个数字,大概是总字节数的1%多一丢丢,可能是固定的吧~
总结
本次关于<audio>兼容性的调测,收获还是巨大的,同时让我认识到了自身的不足!尤其是对HTTP协议这一块的内容,还是需要深挖!里面有很多不为人知的小细节,如果不注意,就会像这次一样,挣扎半天~
写在最后的话
小小打个广告,有大佬们需要短信业务,或者号码认证的能力的话,可以看看这里!中国联通创新能力平台 运营商官方平台!没有中间商赚差价~