聲音無法自動播放這個在IOS/Android上面一直是個慣例,桌面版的Safari在2017年的11版本也宣布禁掉帶有聲音的多媒體自動播放功能,緊接着在2018年4月份發布的Chrome 66也正式關掉了聲音自動播放,也就是說<audio autopaly></audio>
<video autoplay></video>
在桌面版瀏覽器也將失效。
最開始移動端瀏覽器是完全禁止音視頻自動播放的,考慮到了手機的帶寬以及對電池的消耗。但是后來又改了,因為瀏覽器廠商發現網頁開發人員可能會使用GIF動態圖代替視頻實現自動播放,正如IOS文檔所說,使用GIF的帶寬流量是Video(h264)格式的12倍,而播放性能消耗是2倍,所以這樣對用戶反而是不利的。又或者是使用Canvas進行hack,如Android Chrome文檔提到。因此瀏覽器廠商放開了對多媒體自動播放的限制,只要具備以下條件就能自動播放:
(1)沒音頻軌道,或者設置了muted屬性
(2)在視圖里面是可見的,要插入到DOM里面並且不是display: none或者visibility: hidden的,沒有滑出可視區域。
換句話說,只要你不開聲音擾民,且對用戶可見,就讓你自動播放,不需要你去使用GIF的方法進行hack.
桌面版的瀏覽器在近期也使用了這個策略,如升級后的Safari 11的說明:
以及Chrome文檔的說明:
這個策略無疑對視頻網站的沖擊最大,如在Safari打開tudou的提示:
添加了一個設置向導。Chrome的禁止更加人性化,它有一個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).
還有這種混響均衡器的例子:
見這個codepen:Web Audio API: parametric equalizer.
最后,一直以來都是只有移動端的瀏覽器禁掉了音視頻的自動播放,現在桌面版的瀏覽器也開始下手了。瀏覽器這樣做的目的在於,不想讓用戶打開一個頁面就各種廣告或者其它亂七八糟的聲音在播,營造一個純靜的環境。但是瀏覽器也不是一刀切,至少允許音視頻靜音的播放。所以對於視頻來說,可以靜音自動播放,然后加個聲音被關掉的圖標讓用戶點擊打開,再加添加設置向導之類的方法引導用戶設置允許當前網站自動播放。而對於聲音可以用AudioContext的API,只要頁面被點過一次AudioContext就被激活了,就能直接在代碼里面控制播放了。