踩坑:移动端audio标签,成果:一款移动端vue-audio标签

5,181 阅读9分钟

前言

最近受人所托,一直在研究关于移动端<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"

没错,只有设置了这个响应头,就能正确拖动进度条,那么为什么呢?知其然,不知其所以然,那和不知有什么区别! 继续查资料!

首先来看一下未设置该响应头的情况,我们的请求头和相应头是怎么样的。

可以看到我们的请求头是有一个range字段的,去请求我们的服务器,而我们的服务器返回的响应头是没有对应的字段来处理这个请求头的,于是浏览器自然也不知道要如何处理这个请求了,那么我们来看一下"Accept-Ranges":"bytes"到底是个什么!

响应头Accept-Ranges:

我们来看看MDN对于这个请求的说法:

当浏览器发现"Accept-Ranges"头时,可以尝试继续中断了的下载,而不是重新开始。

接下来我们看调试界面:

很明显,虽然发送了请求,但是由于未设置"Accept-Ranges"所以浏览器并没有继续下载,如果你不点击播放,浏览器也就不会再向服务器去请求数据。

也就是说,如果我们我们没有正确的设置"Accept-Ranges",浏览器会认为我们将重新开始下载该视频,而不是从断点处继续进行下载,自然也不存在缓存之类的说法,所以浏览器就会禁止我们人为的设置currentTime(虽然点击播放键后,开始下载视频了,但是响应头有keepalive,所以浏览器认为是一次链接,所以currentTime仍然不能设置,其实哪怕超时断开连接,只要你的响应头没有"Accept-Ranges":"bytes"还是会不让你设置的,貌似IE11可以设置了,有大佬愿意去试试么,嘿嘿)

这里贴两篇参考资料,不一定是最好的,但是对我很有帮助!

www.php.cn/js-tutorial…

www.cnblogs.com/simonbaker/…

三、第三次调试

有了上述的结论,喜出望外!是不是成功了呢!开始手机调试,安卓成功!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了!见下图请求头:

那么到底要返回怎么样的响应头,IOS才会理你呢?

不卖关子,就是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协议这一块的内容,还是需要深挖!里面有很多不为人知的小细节,如果不注意,就会像这次一样,挣扎半天~

写在最后的话

小小打个广告,有大佬们需要短信业务,或者号码认证的能力的话,可以看看这里!中国联通创新能力平台 运营商官方平台!没有中间商赚差价~