Chrome 66禁止聲音自動(dòng)播放之后
聲音無(wú)法自動(dòng)播放這個(gè)在IOS/Android上面一直是個(gè)慣例,桌面版的Safari在2017年的11版本也宣布禁掉帶有聲音的多媒體自動(dòng)播放功能,緊接著在2018年4月份發(fā)布的Chrome 66也正式關(guān)掉了聲音自動(dòng)播放,也就是說<audio autopaly></audio> <video autoplay></video>在桌面版瀏覽器也將失效。
最開始移動(dòng)端瀏覽器是完全禁止音視頻自動(dòng)播放的,考慮到了手機(jī)的帶寬以及對(duì)電池的消耗。但是后來(lái)又改了,因?yàn)闉g覽器廠商發(fā)現(xiàn)網(wǎng)頁(yè)開發(fā)人員可能會(huì)使用GIF動(dòng)態(tài)圖代替視頻實(shí)現(xiàn)自動(dòng)播放,正如IOS文檔所說,使用GIF的帶寬流量是Video(h264)格式的12倍,而播放性能消耗是2倍,所以這樣對(duì)用戶反而是不利的。又或者是使用Canvas進(jìn)行hack,如Android Chrome文檔提到。因此瀏覽器廠商放開了對(duì)多媒體自動(dòng)播放的限制,只要具備以下條件就能自動(dòng)播放:
(1)沒音頻軌道,或者設(shè)置了muted屬性
(2)在視圖里面是可見的,要插入到DOM里面并且不是display: none或者visibility: hidden的,沒有滑出可視區(qū)域。
換句話說,只要你不開聲音擾民,且對(duì)用戶可見,就讓你自動(dòng)播放,不需要你去使用GIF的方法進(jìn)行hack.
桌面版的瀏覽器在近期也使用了這個(gè)策略,如升級(jí)后的Safari 11的說明:
以及Chrome文檔的說明:
這個(gè)策略無(wú)疑對(duì)視頻網(wǎng)站的沖擊***,如在Safari打開tudou的提示:
添加了一個(gè)設(shè)置向?qū)?。Chrome的禁止更加人性化,它有一個(gè)MEI的策略,這個(gè)策略大概是說只要用戶在當(dāng)前網(wǎng)頁(yè)主動(dòng)播放過超過7s的音視頻(視頻窗口不能小于200 x 140),就允許自動(dòng)播放。
對(duì)于網(wǎng)頁(yè)開發(fā)人員來(lái)說,應(yīng)當(dāng)如何有效地規(guī)避這個(gè)風(fēng)險(xiǎn)呢?
Chrome的文檔給了一個(gè)***實(shí)踐:先把音視頻加一個(gè)muted的屬性就可以自動(dòng)播放,然后再顯示一個(gè)聲音被關(guān)掉的按鈕,提示用戶點(diǎn)一下打開聲音。對(duì)于視頻來(lái)說,確實(shí)可以這樣處理,而對(duì)于音頻來(lái)說,很多人是監(jiān)聽頁(yè)面點(diǎn)擊事件,只要點(diǎn)一次了就開始播放聲音,一般就是播放個(gè)背景音樂。但是如果對(duì)于有多個(gè)聲音資源的頁(yè)面來(lái)說如何自動(dòng)播放多個(gè)聲音呢?
首先,如果用戶還沒進(jìn)行交互就調(diào)用播放聲音的API,Chrome會(huì)這么提示:
- DOMException: play() failed because the user didn’t interact with the document first.
Safari會(huì)這么提示:
NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.
Chrome報(bào)錯(cuò)提示最為友善,意思是說,用戶還沒有交互,不能調(diào)play。用戶的交互包括哪些呢?包括用戶觸發(fā)的touchend, click, doubleclick或者是 keydown事件,在這些事件里面就能調(diào)play.
所以上面提到很多人是監(jiān)聽整個(gè)頁(yè)面的點(diǎn)擊事件進(jìn)行播放,不管點(diǎn)的哪里,只要點(diǎn)了就行,包括觸摸下滑。這種方法只適用于一個(gè)聲音資源,不適用多個(gè)聲音,多個(gè)聲音應(yīng)該怎么破呢?這里并不是說要和瀏覽器對(duì)著干,“逆天而行”,我們的目的還是為了提升用戶體驗(yàn),因?yàn)橛行﹫?chǎng)景如果能自動(dòng)播放確實(shí)比較好,如一些答題的場(chǎng)景,需要聽聲音進(jìn)行答題,如果用戶在答題的過程中能依次自動(dòng)播放相應(yīng)題目的聲音,確實(shí)比較方便。同時(shí)也是討論聲音播放的技術(shù)實(shí)現(xiàn)。
原生播放視頻應(yīng)該就只能使用video標(biāo)簽,而原生播放音頻除了使用audio標(biāo)簽之外,還有另外一個(gè)API叫AudioContext,它是能夠用來(lái)控制聲音播放并帶了很多豐富的操控接口。調(diào)audio.play必須在點(diǎn)擊事件里面響應(yīng),而使用AudioContext的區(qū)別在于只要用戶點(diǎn)過頁(yè)面任何一個(gè)地方之后就都能播放了。所以可以用AudioContext取代audio標(biāo)簽播放聲音。
我們先用audio.play檢測(cè)頁(yè)面是否支持自動(dòng)播放,以便決定我們播放的時(shí)機(jī)。
1. 頁(yè)面自動(dòng)播放檢測(cè)
方法很簡(jiǎn)單,就是創(chuàng)建一個(gè)audio元素,給它賦一個(gè)src,append到dom里面,然后調(diào)用它的play,看是否會(huì)拋異常,如果捕獲到異常則說明不支持,如下代碼所示:
- function testAutoPlay () {
- // 返回一個(gè)promise以告訴調(diào)用者檢測(cè)結(jié)果
- return new Promise(resolve => {
- let audio = document.createElement('audio');
- // require一個(gè)本地文件,會(huì)變成base64格式
- audio.src = require('@/assets/empty-audio.mp3');
- document.body.appendChild(audio);
- let autoplay = true;
- // play返回的是一個(gè)promise
- audio.play().then(() => {
- // 支持自動(dòng)播放
- autoplay = true;
- }).catch(err => {
- // 不支持自動(dòng)播放
- autoplay = false;
- }).finally(() => {
- audio.remove();
- // 告訴調(diào)用者結(jié)果
- resolve(autoplay);
- });
- });
- }
這里使用一個(gè)空的音頻文件,它是一個(gè)時(shí)間長(zhǎng)度為0s的mp3文件,大小只有4kb,并且通過webpack打包成本地的base64格式,所以不用在canplay事件之后才調(diào)用play,直接寫成同步代碼,如果src是一個(gè)遠(yuǎn)程的url,那么就得監(jiān)聽canplay事件,然后在里面play.
在告訴調(diào)用者結(jié)果時(shí),使用Promise resolve的方式,因?yàn)閜lay的結(jié)果是異步的,并且?guī)旌瘮?shù)里面不推薦使用await。
2. 監(jiān)聽頁(yè)面交互點(diǎn)擊
如果當(dāng)前頁(yè)面能夠自動(dòng)播放,那么可以毫無(wú)顧忌地讓聲音自動(dòng)播放了,否則就得等到用戶開始和這個(gè)頁(yè)面交互了即有點(diǎn)擊操作了之后才能自動(dòng)播放,如下代碼所示:
- let audioInfo = {
- autoplay: false,
- testAutoPlay () {
- // 代碼同,略...
- },
- // 監(jiān)聽頁(yè)面的點(diǎn)擊事件,一旦點(diǎn)過了就能autoplay了
- setAutoPlayWhenClick () {
- function setAutoPlay () {
- // 設(shè)置自動(dòng)播放為true
- audioInfo.autoplay = true;
- document.removeEventListener('click', setAutoPlay);
- document.removeEventListener('touchend', setAutoPlay);
- }
- document.addEventListener('click', setCallback);
- document.addEventListener('touchend', setCallback);
- },
- init () {
- // 檢測(cè)是否能自動(dòng)播放
- audioInfo.testAutoPlay().then(autoplay => {
- if (!audioInfo.autoplay) {
- audioInfo.autoplay = autoplay;
- }
- });
- // 用戶點(diǎn)擊交互之后,設(shè)置成能自動(dòng)播放
- audioInfo.setAutoPlayWhenClick();
- }
- };
- audioInfo.init();
- export default audioInfo;
上面代碼主要監(jiān)聽document的click事件,在click事件里面把a(bǔ)utoplay值置為true。換句話說,只要用戶點(diǎn)過了,我們就能隨時(shí)調(diào)AudioContext的播放API了,即使不是在點(diǎn)擊事件響應(yīng)函數(shù)里面,雖然無(wú)法在異步回調(diào)里面調(diào)用audio.play,但是AudioContext可以做到。
代碼***通過調(diào)用audioInfo.init,把能夠自動(dòng)播放的信息存儲(chǔ)在了audioInfo.autoplay這個(gè)變量里面。當(dāng)需要播放聲音的時(shí)候,例如切到了下一題,需要自動(dòng)播放當(dāng)前題的幾個(gè)音頻資源,就取這個(gè)變量判斷是否能自動(dòng)播放,如果能就播,不能就等用戶點(diǎn)聲音圖標(biāo)自己去播,并且如果他點(diǎn)過了一次之后就都能自動(dòng)播放了。
那么怎么用AudioContext播放聲音呢?
3. AudioContext播放聲音
先請(qǐng)求音頻文件,放到ArrayBuffer里面,然后用AudioContext的API進(jìn)行decode解碼,解碼完了再讓它去play,就行了。
我們先寫一個(gè)請(qǐng)求音頻文件的ajax:
- function request (url) {
- return new Promise (resolve => {
- let xhr = new XMLHttpRequest();
- xhr.open('GET', url);
- // 這里需要設(shè)置xhr response的格式為arraybuffer
- // 否則默認(rèn)是二進(jìn)制的文本格式
- xhr.responseType = 'arraybuffer';
- xhr.onreadystatechange = function () {
- // 請(qǐng)求完成,并且成功
- if (xhr.readyState === 4 && xhr.status === 200) {
- resolve(xhr.response);
- }
- };
- xhr.send();
- });
- }
這里需要注意的是要把xhr響應(yīng)類型改成arraybuffer,因?yàn)閐ecode需要使用這種存儲(chǔ)格式,這樣設(shè)置之后,xhr.response就是一個(gè)ArrayBuffer格式了。
接著實(shí)例化一個(gè)AudioContext,讓它去解碼然后play,如下代碼所示:
- // Safari是使用webkit前綴
- let context = new (window.AudioContext || window.webkitAudioContext)();
- // 請(qǐng)求音頻數(shù)據(jù)
- let audioMedia = await request(url);
- // 進(jìn)行decode和play
- context.decodeAudioData(audioMedia, decode => play(context, decode));
play的函數(shù)實(shí)現(xiàn)如下:
- function play (context, decodeBuffer) {
- let source = context.createBufferSource();
- source.buffer = decodeBuffer;
- source.connect(context.destination);
- // 從0s開始播放
- source.start(0);
- }
這樣就實(shí)現(xiàn)了AudioContext播放音頻的基本功能。
如果當(dāng)前頁(yè)面是不能autoplay,那么在 new AudioContext的時(shí)候,Chrome控制臺(tái)會(huì)報(bào)一個(gè)警告:
這個(gè)的意思是說,用戶還沒有和頁(yè)面交互你就初始化了一個(gè)AudioContext,我是不會(huì)讓你play的,你需要在用戶點(diǎn)擊了之后resume恢復(fù)這個(gè)context才能夠進(jìn)行play.
假設(shè)我們不管這個(gè)警告,直接調(diào)用play沒有報(bào)錯(cuò),但是沒有聲音。所以這個(gè)時(shí)候就要用到上一步audioInfo.autoplay的信息,如果這個(gè)為true,那么可以play,否則不能play,需要讓用戶自己點(diǎn)聲音圖標(biāo)進(jìn)行播放。所以,把代碼重新組織一下:
- function play (context, decodeBuffer) {
- // 調(diào)用resume恢復(fù)播放
- 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)();
- // 如果能夠自動(dòng)播放
- if (audioInfo.autoplay) {
- playAudio(url);
- }
- // 支持用戶點(diǎn)擊聲音圖標(biāo)自行播放
- $('.audio-icon').on('click', function () {
- playAudio($(this).data('url'));
- });
調(diào)了resume之后,如果之前有被禁止播放的音頻就會(huì)開始播放,如果沒有則直接恢復(fù)context的自動(dòng)播放功能。這樣就達(dá)到基本目的,如果支持自動(dòng)播放就在代碼里面直接play,不支持就等點(diǎn)擊。只要點(diǎn)了一次,不管點(diǎn)的哪里接下來(lái)的都能夠自動(dòng)播放了。就能實(shí)現(xiàn)類似于每隔3s自動(dòng)播下一題的音頻的目的:
- // 每隔3秒自動(dòng)播放一個(gè)聲音
- playAudio('question-1.mp3');
- setTimeout(() => playAudio(context, 'question-2.mp3'), 3000);
- setTimeout(() => playAudio(context, 'question-3.mp3'), 3000);
這里還有一個(gè)問題,怎么知道每個(gè)聲音播完了,然后再隔個(gè)3s播放下一個(gè)聲音呢?可以通過兩個(gè)參數(shù),一個(gè)是解碼后的decodeBuffer有當(dāng)前音頻的時(shí)長(zhǎng)duration屬性,而通過context.currentTime可以知道當(dāng)前播放時(shí)間精度,然后就可以弄一個(gè)計(jì)時(shí)器,每隔100ms比較一下context.currentTime是否大于docode.duration,如果是的話說明播完了。soundjs這個(gè)庫(kù)就是這么實(shí)現(xiàn)的,我們可以利用這個(gè)庫(kù)以方便對(duì)聲音的操作。
這樣就實(shí)現(xiàn)了利用AudioContext自動(dòng)播放多個(gè)音頻的目的,限制是用戶***打開頁(yè)面是不能自動(dòng)播放的,但是一旦用戶點(diǎn)過頁(yè)面的任何一個(gè)地方就可以了。
AudioContext還有其它的一些操作。
4. AudioContext控制聲音屬性
例如這個(gè)CSS Tricks列了幾個(gè)例子,其中一個(gè)是利用AudioContext的振蕩器oscillator寫了一個(gè)電子木琴:
這個(gè)例子沒有用到任何一個(gè)音頻資源,都是直接合成的,感受如這個(gè)Demo:Play the Xylophone (Web Audio API).
還有這種混響均衡器的例子:
見這個(gè)codepen:Web Audio API: parametric equalizer.
***,一直以來(lái)都是只有移動(dòng)端的瀏覽器禁掉了音視頻的自動(dòng)播放,現(xiàn)在桌面版的瀏覽器也開始下手了。瀏覽器這樣做的目的在于,不想讓用戶打開一個(gè)頁(yè)面就各種廣告或者其它亂七八糟的聲音在播,營(yíng)造一個(gè)純靜的環(huán)境。但是瀏覽器也不是一刀切,至少允許音視頻靜音的播放。所以對(duì)于視頻來(lái)說,可以靜音自動(dòng)播放,然后加個(gè)聲音被關(guān)掉的圖標(biāo)讓用戶點(diǎn)擊打開,再加添加設(shè)置向?qū)е惖姆椒ㄒ龑?dǎo)用戶設(shè)置允許當(dāng)前網(wǎng)站自動(dòng)播放。而對(duì)于聲音可以用AudioContext的API,只要頁(yè)面被點(diǎn)過一次AudioContext就被激活了,就能直接在代碼里面控制播放了。
以上可作為當(dāng)前網(wǎng)頁(yè)多媒體播放的***實(shí)踐參考。
【本文是51CTO專欄作者“人人網(wǎng)FED”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過51CTO聯(lián)系原作者獲取授權(quán)】