- Published on
手机浏览器上视频和音频自动播放问题
- Authors
- Name
- lcorz
记录一个线上 bug!
问题场景:
iphone6 safari 浏览器打开爱奇艺电视剧播放页,如你好,旧时光,然后直接点第二集,播放器一直显示 loading 浮层,如下图所示
image.png
预期结果应该是可以正常加载第二集视频并播放的。但是如果你进入播放页后,先点击第一集的播放按钮,等待视频播放后,再切第二集,又是可以正常播放的。
这是一个兼容性问题,因为用 iphone6 qq 浏览器无法复现。因此我首先作了一个统计,看哪些浏览器可以复现,针对 ios 和 andorid 各选了几款机型,统计如下:
- 机型:iphone8/iphone x/iphone7P,系统:ios11+ safari、qq、百度、手百均没有问题
- 机型:iphone6,系统:ios10 qq 没有问题,safari、百度、手百有问题
- 机型:iphone5,系统:ios8.1 qq 没有问题,safari、百度、手百有问题
- 机型:小米 6,系统:android7 qq、小米浏览器没有问题,chrome、百度、手百有问题
- 机型:oppo r9,系统:android6 qq 没有问题,chrome、百度、手百、系统自带浏览器有问题
从以上来看,安卓 chrome、百度、手百以及 ios10 以下的 safari、百度和手百均是有问题的,QQ 浏览器比较特殊,在 android 和 ios 均表现正常。
视频播放的过程
我们先分析下播放视频的流程,首先会先向后台发送鉴权请求,其实就是根据你的用户信息,判断是否播广告、试看 6 分钟或是否要用券等等信息,比较复杂,但最终而言,都是为了拿到一个视频的播放链接(如果是鉴权后无法播放的,那就需要显示播放器的提示浮层了,暂不考虑这种情况)。然后再分析以下几种操作场景的区别: 1. 打开播放页,直接点封面图的播放按钮 打开页面的过程中,js 会提前发送鉴权请求拿到播放的 url 地址(这是为了缩短用户开播时间而做的优化),然后将 url 赋值给 video 标签的 src 属性,所以打开页面后,video 标签其实已经赋上第一集的播放地址了。然后我们点击封面图的播放按钮,执行点击事件的函数,最终调用 video 对象的 play()方法,从而正常播放视频。所以这一步,没有请求接口拿播放地址的操作。
2.打开播放页,点击封面图的播放按钮,播放后,再切点击第二集 前面的操作是一样的,来看看点击第二集的操作过程,其实比较类似,点击后需要先发鉴权请求拿到第二集的播放 url,然后将 url 地址赋值到 video 的 src 属性,最终调用 video 的 play()方法播放视频。到现在为止,一切还都正常, 符合我们的预期,接下来看看第三种操作。
3. 打开播放页,直接点击第二集 这一次,我们没有先播放第一集,而是直接想播放第二集视频,结果就 GG 了,播放器一直显示 loading 浮层。对于 loading 浮层的显示逻辑,应该是点击切换剧集就会显示,然后待拿到播放 url,赋值 src 属性,调用 play()播放视频后就应该隐藏的,实现方式就是监听播放器的 playing 事件,一般播放器发送 playing 事件就表示视频第一帧已经播放了。所以,loading 浮层不消失的原因应该是播放器没有触发 playing 事件,或者说视频没有正常播放。
我们对比一下操作场景 1 和场景 3 的区别,场景 1 在点击按钮时,video 标签的 src 属性已经赋上,而场景 3 点击剧集,需要调用一个后台接口拿到播放链接,然后赋值到 src,调用 play(),所以两者的区别应该是多了一次请求鉴权的过程,这个请求需要一定的时间,所以我们猜想,是不是这个时间太长导致。
为了模拟以上几种场景,写了一个简单的页面进行模拟。demo 非常简单,就是一个播放器,加几个基本的按钮,可以实现视频的播放、暂停与切换。
基本代码如下:
<!DOCTYPE html>
<html>
<head>
<title>video兼容性测试</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">
</head>
<script type="text/javascript" src="http://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script>
<script type="text/javascript" src="//static.qiyi.com/js/html5/js/lib/lib.2.0.1.min.js?sea1.2.min.js"></script>
<style>
#num {
width: 100%;
height: 100%;
text-align: center;
font-size: 48px;
font-weight: bold;
}
</style>
<body>
<video id="video" src="http://qamp.qiyi.domain/static/03.mp4" width="100%" height="100%"
controls poster="http://qamp.qiyi.domain/static/03.jpg">
不支持video标签显示这段文字
</video>
<div>
<button type="button" onclick="play()"> 播放 </button>
<button type="button" onclick="pause()"> 暂停 </button>
<button type="button" id="video1" onclick="playVideo(this.id)"> 第一集 </button>
<button type="button" id="video2" onclick="playVideo(this.id)"> 第二集 </button>
<button type="button" id="video3" onclick="playVideo(this.id)"> 第三集 </button>
<img src="http://qamp.qiyi.domain/static/loading.jpg" alt="" style="display:none">
<div id="num"></div>
</div>
<script>
albumData = {
'video1': {
'src' : 'http://qamp.qiyi.domain/static/01.mp4',
'img' : 'http://qamp.qiyi.domain/static/01.jpg'
},
'video2': {
'src' : 'http://qamp.qiyi.domain/static/02.mp4',
'img' : 'http://qamp.qiyi.domain/static/02.jpg'
},
'video3': {
'src' : 'http://qamp.qiyi.domain/static/03.mp4',
'img' : 'http://qamp.qiyi.domain/static/03.jpg'
}
}
function sleep(d){
for(var t = Date.now();Date.now() - t <= d*1000;){};
}
function play() {
$('video')[0].play()
}
function pause() {
$('video')[0].pause()
}
function playVideo(tvid) {
sleep(1.51)
video = $('video')[0]
video.src = albumData[tvid]['src']
video.poster = albumData[tvid]['img']
//setTimeout(() => video.play(),0)
video.play()
$('#num').text('playing');
}
</script>
</body>
</html>
最后发现部分移动端浏览器对于视频的播放有如下限制条件:
- 从触发事件开始,到调用 video 对象的 play()方法,时间上不能超过 1000ms
- setInterval 函数里,只有第一次就调用 play()才能成功,原因不明
为什么手机端浏览器会有这个限制
这大概要从手机流量说起,我记得在上大学那会,2008 年左右,那时候还是 PC 时代,智能机还不怎么流行,流量也是非常昂贵,5 块钱大概才 30M,所以为了用户着想,浏览器禁止了 autoplay 和 js 的加载播放,防止流量偷跑的现象,而且在移动流量下播放视频,即使是手动触发视频播放,浏览器也会再次弹窗让用户确认,确认后才放行操作。但随着流量越来越便宜,苹果在 IOS11 版本取消了该限制。 以下摘自苹果的文档:
In Safari on iOS (for all devices, including iPad), where the user may be on a cellular network and be charged per data unit, preload and autoplay are disabled. No data is loaded until the user initiates it. This means the JavaScript play() and load() methods are also inactive until the user initiates playback, unless the play() or load() method is triggered by user action. In other words, a user-initiated Play button works, but an onLoad="play()" event does not.
android4.2 也引入了与 ios 类似的方案,由 mediaPlaybackRequiresUserAction 参数控制,默认是 YES,所以必须由用户触发的行为,调用 play()方法才有效,而 onload 事件是没法播放的,这也就是为什么手机浏览器无法通过 js 控制自动播放。 详细文档参考:http://fqk.io/issues-of-audio-video-in-webview/
时间上的限制
为了确定具体的时间限制。在 demo 代码中有一个 sleep 函数,可以控制从点击到调 play()方法的时间,在各个浏览器上实验后,统计如下:
- Android 端浏览器: QQ 浏览器:60+s(暂不知道上限) chrome/百度/手百/360 极速: 1000ms
- IOS 端浏览器: QQ 浏览器:60+s(暂不知道上限) ios 10 以下的 safari/chrome/百度/手百: 1000ms ios11 以上 safari/chrome/百度/手百:无限制
于是统计了下爱奇艺电视剧播放页,切换剧集时,从点击到开播广告,大概是 1200ms 左右,所以超过了 1s 的时间限制,导致 play()被浏览器视为非用户行为,所以无法播放。
注:如果成功播放视频后,之后的操作就没有时间限制了,所以当进入页面后先播放第一集,然后再切第二集,就不会一直 loading 了
关于在 setInterval 函数里调用 play()
发现这个问题纯属巧合,至今也没明白为什么在 setInterval 函数里调用 play()会失败。
在写 demo 的时候,本想用 setInterval 实现一个倒计时的功能,setInterval 会每隔一定时间调用某个固定函数 intervalFunc,最后发现只有在第一次调用的 intervalFunc 函数里执行 video.play()才有效,后面再调用的都无法播放视频,即使设置的时间很短也不行,代码如下:
function playVideo(tvid) {
var count = 20 //单位ms
var intervalTime = 10 //每隔多久执行一次
$('#num').text(count);
var inter = setInterval(function intervalFunc() {
count = count - intervalTime ;
$('#num').text(count);
if (count == 0) {
//只有第一次就走到这分支才能播放
clearInterval(inter);
video = $('#video')[0]
video.play()
video.pause()
video.src = albumData[tvid]['src']
video.poster = albumData[tvid]['img']
//setTimeout(() => video.play(),0)
video.play()
$('#num').text('playing');
}
}, intervalTime);
}
上面代码大概意思是从 20ms 开始倒计时,每隔 10ms 递减一次,每次减 10ms,等到为 0 时,执行 play(),但是失败了,20ms 如果是放在同步执行的代码里,是没有问题的。 猜想可能是因为后面执行的 intervalFunc 被认为是非用户触发的,所以被 play 请求被拦截了。 在看 H5 站源代码时候,里面发送请求的代码都用了 promise 异步写法,于是改造了一下 demo,如下:
function getSrc(tvid) {
sleep(1.9)
return new Promise(function (resolve, reject) {
console.log(albumData[tvid])
resolve(albumData[tvid])
})
}
function playVideo(tvid){
getSrc(tvid).then((data)=>{
video = $('video')[0]
video.src = data['src']
video.poster = data['img']
video.play()
})
}
改完后在小米 6 测试,发现用 promise 的写法是没问题的,结果一致,只有 sleep 超过 1s 才会出现问题。 然后在 stackflow 发现这样一篇文章,play-request-was-interrupted,里面说
image.png
大概意思就是,绑定点击事件的函数,不要使用 async 异步写法,否则会失去之后允许视频播放的用户行为 token,也就是会被浏览器认定为非人为操作导致无法播放。 于是我将点击函数改为 async 异步模式,代码如下:
function getSrc(tvid) {
sleep(3.99)
return new Promise(function (resolve, reject) {
console.log(albumData[tvid])
resolve(albumData[tvid])
})
}
async function playVideo(tvid){
await getSrc(tvid).then((data)=>{
video = $('video')[0]
video.src = data['src']
video.poster = data['img']
video.play()
$('#num').text('playing');
})
}
在小米 6 上测试,发现使用 async 也是没问题的,只有在超过 1s 才会播放失败。
如何解决视频自动播放的限制
如果产品非得要求做到自动播放,该如何做呢?答案是在点击后立即调用 play()方法,如果 video 原本有 src 的话,会播放之前视频,所以要立马调用 pause()方法,但有可能会报 Uncaught (in promise) DOMException: The play() request was interrupted by a call to pause(),这个错倒不要紧,无关紧要,参考play() request was interrupted,要暂停视频,也可以先将 video 的 src 清空,这样也是可以的。 代码如下:
function getSrc(tvid) {
sleep(3.99)
return new Promise(function (resolve, reject) {
console.log(albumData[tvid])
resolve(albumData[tvid])
})
}
async function playVideo(tvid){
//立即调用play
try{
$('video')[0].play()
$('video')[0].pause()
} catch(e){
console.log(e)
}
await getSrc(tvid).then((data)=>{
video = $('video')[0]
video.src = data['src']
video.poster = data['img']
video.play()
$('#num').text('playing');
})
}
真正解决手机视频自动播放问题
http://levisft.beats-digital.com http://campaign.wandoujia.com/market/ifly/index.html?ch_src_share=pp_aty_wechatshare https://yq.aliyun.com/mk/01/index.php http://nigg.treedom.cn/?dskid=ccc003 http://m.creatby.com/v2/manage/book/bcz2jo/ http://h5.flyfinger.com/2016/tencentfun/ https://lieyu.qq.com/cp/a20170213ane/index-wx.html? http://jzsg.lxustudio.cn/
说说音频的自动播放问题
目前很多 H5 活动页面都会带有背景音乐,但音频也有类似的问题,具体的表现是在大多数安卓机上是可以自动播放,但是在 ios 上会有限制,那该如何解决呢?
1. 方法一:监听 touchstart 事件,用户触摸屏幕后会自动播放
document.addEventListener('touchstart', function(){
audio.play();
}, false);
方法二:依赖微信的 ready 事件进行,可以解决 ios 微信的问题
document.addEventListener("WeixinJSBridgeReady", function () {
audio.play();
}, false);
参考文献:
https://developers.google.com/web/updates/2017/06/play-request-was-interrupted https://segmentfault.com/a/1190000007864808