Chrome 66禁止聲音自動播放之後

人人網FED發表於2018-05-12

聲音無法自動播放這個在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.

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就被啟用了,就能直接在程式碼裡面控制播放了。

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

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


相關文章