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就被激活了,就能直接在代碼裏面控制播放了。

以上可做爲當前網頁多媒體播放的最佳實踐參考。

【號外】《高效前端》準備第二次印刷,據說你還沒買

相關文章
相關標籤/搜索