声音无法自动播放这个在 IOS/Android 上面一直是个惯例, 桌面版的 Safari 在 2017 年的 11 版本也宣布禁掉带有声音的多媒体自动播放功能, 紧接着在 2018 年 4 月份发布的 Chrome 66 也正式关掉了声音自动播放, 也就是说
- <audio autopaly></audio>
- <video autoplay></video>
在桌面版浏览器也将失效.
最开始移动端浏览器是完全禁止音视频自动播放的, 考虑到了手机的带宽以及对电池的消耗. 但是后来又改了, 因为浏览器厂商发现网页开发人员可能会使用 GIF 动态图代替视频实现自动播放, 正如 IOS 文档 https://webkit.org/blog/6784/new-video-policies-for-ios/ 所说, 使用 GIF 的带宽流量是 Video(h264) 格式的 12 倍, 而播放性能消耗是 2 倍, 所以这样对用户反而是不利的. 又或者是使用 Canvas 进行 hack, 如 Android Chrome 文档 https://developers.google.com/web/updates/2016/07/autoplay 提到. 因此浏览器厂商放开了对多媒体自动播放的限制, 只要具备以下条件就能自动播放:
(1) 没音频轨道, 或者设置了 muted 属性
(2) 在视图里面是可见的, 要插入到 DOM 里面并且不是 display: none 或者 visibility: hidden 的, 没有滑出可视区域.
换句话说, 只要你不开声音扰民, 且对用户可见, 就让你自动播放, 不需要你去使用 GIF 的方法进行 hack.
桌面版的浏览器在近期也使用了这个策略, 如升级后的 Safari 11 的说明:
以及 Chrome 文档的说明 https://developers.google.com/web/updates/2017/09/autoplay-policy-changes :
这个策略无疑对视频网站的冲击最大, 如在 Safari 打开 tudou 的提示:
添加了一个设置向导. Chrome 的禁止更加人性化, 它有一个 MEI 的策略 https://developers.google.com/web/updates/2017/09/autoplay-policy-changes#mei , 这个策略大概是说只要用户在当前网页主动播放过超过 7s 的音视频 (视频窗口不能小于 200 x 140), 就允许自动播放.
对于网页开发人员来说, 应当如何有效地规避这个风险呢?
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, 是因为在给别人调用的库函数里面不应该使用 await, 由调用者自行决定是否要 await, 不然库函数就变成同步的代码, 就得强制别人去 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) https://codepen.io/yincheng/pen/JvZPYJ .
还有这种混响均衡器的例子:
见这个 codepen:Web Audio API: parametric equalizer https://codepen.io/yincheng/pen/ZoRzBO .
最后, 一直以来都是只有移动端的浏览器禁掉了音视频的自动播放, 现在桌面版的浏览器也开始下手了. 浏览器这样做的目的在于, 不想让用户打开一个页面就各种广告或者其它乱七八糟的声音在播, 营造一个纯静的环境. 但是浏览器也不是一刀切, 至少允许音视频静音的播放. 所以对于视频来说, 可以静音自动播放, 然后加个声音被关掉的图标让用户点击打开, 再加添加设置向导之类的方法引导用户设置允许当前网站自动播放. 而对于声音可以用 AudioContext 的 API, 只要页面被点过一次 AudioContext 就被激活了, 就能直接在代码里面控制播放了.
以上可作为当前网页多媒体播放的最佳实践参考.
来源: http://news.51cto.com/art/201805/573266.htm