Chrome 66禁止声音自动播放以后

声音没法自动播放这个在IOS/Android上面一直是个惯例,桌面版的Safari在2017年的11版本也宣布禁掉带有声音的多媒体自动播放功能,紧接着在2018年4月份发布的Chrome 66也正式关掉了声音自动播放,也就是说<audio autopaly></audio> <video autoplay></video>在桌面版浏览器也将失效。javascript

最开始移动端浏览器是彻底禁止音视频自动播放的,考虑到了手机的带宽以及对电池的消耗。可是后来又改了,由于浏览器厂商发现网页开发人员可能会使用GIF动态图代替视频实现自动播放,正如IOS文档所说,使用GIF的带宽流量是Video(h264)格式的12倍,而播放性能消耗是2倍,因此这样对用户反而是不利的。又或者是使用Canvas进行hack,如Android Chrome文档提到。所以浏览器厂商放开了对多媒体自动播放的限制,只要具有如下条件就能自动播放:css

(1)没音频轨道,或者设置了muted属性html

(2)在视图里面是可见的,要插入到DOM里面而且不是display: none或者visibility: hidden的,没有滑出可视区域。前端

换句话说,只要你不开声音扰民,且对用户可见,就让你自动播放,不须要你去使用GIF的方法进行hack.java

桌面版的浏览器在近期也使用了这个策略,如升级后的Safari 11的说明:webpack

以及Chrome文档的说明ios

这个策略无疑对视频网站的冲击最大,如在Safari打开tudou的提示:web

添加了一个设置向导。Chrome的禁止更加人性化,它有一个MEI的策略,这个策略大概是说只要用户在当前网页主动播放过超过7s的音视频(视频窗口不能小于200 x 140),就容许自动播放。ajax


对于网页开发人员来讲,应当如何有效地规避这个风险呢?api

Chrome的文档给了一个最佳实践:先把音视频加一个muted的属性就能够自动播放,而后再显示一个声音被关掉的按钮,提示用户点一下打开声音。对于视频来讲,确实能够这样处理,而对于音频来讲,不少人是监听页面点击事件,只要点一次了就开始播放声音,通常就是播放个背景音乐。可是若是对于有多个声音资源的页面来讲如何自动播放多个声音呢?

首先,若是用户还没进行交互就调用播放声音的API,Chrome会这么提示:

DOMException: play() failed because the user didn't interact with the document first.

Safari会这么提示:

NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.

Chrome报错提示最为友善,意思是说,用户尚未交互,不能调play。用户的交互包括哪些呢?包括用户触发的touchend, click, doubleclick或者是 keydown事件,在这些事件里面就能调play.

因此上面提到不少人是监听整个页面的点击事件进行播放,无论点的哪里,只要点了就行,包括触摸下滑。这种方法只适用于一个声音资源,不适用多个声音,多个声音应该怎么破呢?这里并非说要和浏览器对着干,“逆天而行”,咱们的目的仍是为了提高用户体验,由于有些场景若是能自动播放确实比较好,如一些答题的场景,须要听声音进行答题,若是用户在答题的过程当中能依次自动播放相应题目的声音,确实比较方便。同时也是讨论声音播放的技术实现。

原生播放视频应该就只能使用video标签,而原生播放音频除了使用audio标签以外,还有另一个API叫AudioContext,它是可以用来控制声音播放并带了不少丰富的操控接口。调audio.play必须在点击事件里面响应,而使用AudioContext的区别在于只要用户点过页面任何一个地方以后就都能播放了。因此能够用AudioContext取代audio标签播放声音。

咱们先用audio.play检测页面是否支持自动播放,以便决定咱们播放的时机。

1. 页面自动播放检测

方法很简单,就是建立一个audio元素,给它赋一个src,append到dom里面,而后调用它的play,看是否会抛异常,若是捕获到异常则说明不支持,以下代码所示:

function testAutoPlay () {
    // 返回一个promise以告诉调用者检测结果
    return new Promise(resolve => {
        let audio = document.createElement('audio');
        // require一个本地文件,会变成base64格式
        audio.src = require('@/assets/empty-audio.mp3');
        document.body.appendChild(audio);
        let autoplay = true;
        // play返回的是一个promise
        audio.play().then(() => {
            // 支持自动播放
            autoplay = true;
        }).catch(err => {
            // 不支持自动播放
            autoplay = false;
        }).finally(() => {
            audio.remove();
            // 告诉调用者结果
            resolve(autoplay);
        });
    });
}复制代码

这里使用一个空的音频文件,它是一个时间长度为0s的mp3文件,大小只有4kb,而且经过webpack打包成本地的base64格式,因此不用在canplay事件以后才调用play,直接写成同步代码,若是src是一个远程的url,那么就得监听canplay事件,而后在里面play.

在告诉调用者结果时,使用Promise resolve的方式,由于play的结果是异步的,而且库函数不推荐使用await.

2. 监听页面交互点击

若是当前页面可以自动播放,那么能够毫无顾忌地让声音自动播放了,不然就得等到用户开始和这个页面交互了即有点击操做了以后才能自动播放,以下代码所示:

let audioInfo = {
    autoplay: false,
    testAutoPlay () {
        // 代码同,略... 
    },
    // 监听页面的点击事件,一旦点过了就能autoplay了
    setAutoPlayWhenClick () {
        function setAutoPlay () {
            // 设置自动播放为true
            audioInfo.autoplay = true;
            document.removeEventListener('click', setAutoPlay);
            document.removeEventListener('touchend', setAutoPlay);
        }
        document.addEventListener('click', setCallback);
        document.addEventListener('touchend', setCallback);
    },
    init () {
        // 检测是否能自动播放
        audioInfo.testAutoPlay().then(autoplay => {
            if (!audioInfo.autoplay) {
                audioInfo.autoplay = autoplay;
            }
        });
        // 用户点击交互以后,设置成能自动播放
        audioInfo.setAutoPlayWhenClick();
    }
};
audioInfo.init();
export default audioInfo;复制代码

上面代码主要监听document的click事件,在click事件里面把autoplay值置为true。换句话说,只要用户点过了,咱们就能随时调AudioContext的播放API了,即便不是在点击事件响应函数里面,虽然没法在异步回调里面调用audio.play,可是AudioContext能够作到。

代码最后经过调用audioInfo.init,把可以自动播放的信息存储在了audioInfo.autoplay这个变量里面。当须要播放声音的时候,例如切到了下一题,须要自动播放当前题的几个音频资源,就取这个变量判断是否能自动播放,若是能就播,不能就等用户点声音图标本身去播,而且若是他点过了一次以后就都能自动播放了。

那么怎么用AudioContext播放声音呢?

3. AudioContext播放声音

先请求音频文件,放到ArrayBuffer里面,而后用AudioContext的API进行decode解码,解码完了再让它去play,就好了。

咱们先写一个请求音频文件的ajax:

function request (url) {
    return new Promise (resolve => {
        let xhr = new XMLHttpRequest();
        xhr.open('GET', url);
        // 这里须要设置xhr response的格式为arraybuffer
        // 不然默认是二进制的文本格式
        xhr.responseType = 'arraybuffer';
        xhr.onreadystatechange = function () {
            // 请求完成,而且成功
            if (xhr.readyState === 4 && xhr.status === 200) {
                resolve(xhr.response);
            }
        };
        xhr.send();
    });
}复制代码

这里须要注意的是要把xhr响应类型改为arraybuffer,由于decode须要使用这种存储格式,这样设置以后,xhr.response就是一个ArrayBuffer格式了。

接着实例化一个AudioContext,让它去解码而后play,以下代码所示:

// Safari是使用webkit前缀
let context = new (window.AudioContext || window.webkitAudioContext)();
// 请求音频数据
let audioMedia = await request(url);
// 进行decode和play
context.decodeAudioData(audioMedia, decode => play(context, decode));复制代码

play的函数实现以下:

function play (context, decodeBuffer) {
    let source = context.createBufferSource();
    source.buffer = decodeBuffer;
    source.connect(context.destination);
    // 从0s开始播放
    source.start(0);
}复制代码

这样就实现了AudioContext播放音频的基本功能。

若是当前页面是不能autoplay,那么在 new AudioContext的时候,Chrome控制台会报一个警告:

这个的意思是说,用户尚未和页面交互你就初始化了一个AudioContext,我是不会让你play的,你须要在用户点击了以后resume恢复这个context才可以进行play.

假设咱们无论这个警告,直接调用play没有报错,可是没有声音。因此这个时候就要用到上一步audioInfo.autoplay的信息,若是这个为true,那么能够play,不然不能play,须要让用户本身点声音图标进行播放。因此,把代码从新组织一下:

function play (context, decodeBuffer) {
    // 调用resume恢复播放
    context.resume();
    let source = context.createBufferSource();
    source.buffer = decodeBuffer;
    source.connect(context.destination);
    source.start(0);
}

function playAudio (context, url) {
    let audioMedia = await request(url);
    context.decodeAudioData(audioMedia, decode => play(context, decode));
}

let context = new (window.AudioContext || window.webkitAudioContext)();
// 若是可以自动播放
if (audioInfo.autoplay) {
    playAudio(url);
}
// 支持用户点击声音图标自行播放
$('.audio-icon').on('click', function () {
    playAudio($(this).data('url'));
});复制代码

调了resume以后,若是以前有被禁止播放的音频就会开始播放,若是没有则直接恢复context的自动播放功能。这样就达到基本目的,若是支持自动播放就在代码里面直接play,不支持就等点击。只要点了一次,无论点的哪里接下来的都可以自动播放了。就能实现相似于每隔3s自动播下一题的音频的目的:

// 每隔3秒自动播放一个声音
playAudio('question-1.mp3');
setTimeout(() => playAudio(context, 'question-2.mp3'), 3000);
setTimeout(() => playAudio(context, 'question-3.mp3'), 3000);复制代码

这里还有一个问题,怎么知道每一个声音播完了,而后再隔个3s播放下一个声音呢?能够经过两个参数,一个是解码后的decodeBuffer有当前音频的时长duration属性,而经过context.currentTime能够知道当前播放时间精度,而后就能够弄一个计时器,每隔100ms比较一下context.currentTime是否大于docode.duration,若是是的话说明播完了。soundjs这个库就是这么实现的,咱们能够利用这个库以方便对声音的操做。

这样就实现了利用AudioContext自动播放多个音频的目的,限制是用户首次打开页面是不能自动播放的,可是一旦用户点过页面的任何一个地方就能够了。

AudioContext还有其它的一些操做。

4. AudioContext控制声音属性

例如这个CSS Tricks列了几个例子,其中一个是利用AudioContext的振荡器oscillator写了一个电子木琴:

这个例子没有用到任何一个音频资源,都是直接合成的,感觉如这个Demo:Play the Xylophone (Web Audio API).

还有这种混响均衡器的例子:

见这个codepen:Web Audio API: parametric equalizer.


最后,一直以来都是只有移动端的浏览器禁掉了音视频的自动播放,如今桌面版的浏览器也开始下手了。浏览器这样作的目的在于,不想让用户打开一个页面就各类广告或者其它乱七八糟的声音在播,营造一个纯静的环境。可是浏览器也不是一刀切,至少容许音视频静音的播放。因此对于视频来讲,能够静音自动播放,而后加个声音被关掉的图标让用户点击打开,再加添加设置向导之类的方法引导用户设置容许当前网站自动播放。而对于声音能够用AudioContext的API,只要页面被点过一次AudioContext就被激活了,就能直接在代码里面控制播放了。

以上可做为当前网页多媒体播放的最佳实践参考。

【号外】《高效前端》准备第二次印刷,据说你还没买