自訂媒體通知及處理播放清單

François Beaufort
François Beaufort

有了全新的 Media Session API,您現在可以為網頁應用程式正在播放的媒體提供中繼資料,自訂媒體通知。您也可以處理媒體相關事件,例如可能來自通知或媒體鍵的快轉或曲目變更。躍躍欲試嗎?請試用官方的媒體工作階段範例

Chrome 57 版支援 Media Session API (2017 年 2 月推出 Beta 版,2017 年 3 月推出穩定版)。

媒體工作階段重點摘要
相片 由 Michael Alø-Nielsen / CC BY 2.0

給我想要的

您已經瞭解 Media Session API,只是想回來複製和貼上一些樣板程式碼嗎?這就是。

if ('mediaSession' in navigator) {

    navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Never Gonna Give You Up',
    artist: 'Rick Astley',
    album: 'Whenever You Need Somebody',
    artwork: [
        { src: 'https://dummyimage.com/96x96',   sizes: '96x96',   type: 'image/png' },
        { src: 'https://dummyimage.com/128x128', sizes: '128x128', type: 'image/png' },
        { src: 'https://dummyimage.com/192x192', sizes: '192x192', type: 'image/png' },
        { src: 'https://dummyimage.com/256x256', sizes: '256x256', type: 'image/png' },
        { src: 'https://dummyimage.com/384x384', sizes: '384x384', type: 'image/png' },
        { src: 'https://dummyimage.com/512x512', sizes: '512x512', type: 'image/png' },
    ]
    });

    navigator.mediaSession.setActionHandler('play', function() {});
    navigator.mediaSession.setActionHandler('pause', function() {});
    navigator.mediaSession.setActionHandler('seekbackward', function() {});
    navigator.mediaSession.setActionHandler('seekforward', function() {});
    navigator.mediaSession.setActionHandler('previoustrack', function() {});
    navigator.mediaSession.setActionHandler('nexttrack', function() {});
}

進入程式碼

來玩遊戲吧 🎷?

在網頁中加入簡單的 <audio> 元素,並指派多個媒體來源,讓瀏覽器選擇最合適的來源。

<audio controls>
    <source src="audio.mp3" type="audio/mp3"/>
    <source src="audio.ogg" type="audio/ogg"/>
</audio>

如你所知,autoplay 已在 Android 版 Chrome 上停用音訊元素,這表示我們必須使用音訊元素的 play() 方法。這個方法必須由使用者手勢觸發,例如輕觸或滑鼠點擊。也就是監聽 pointerupclicktouchend 事件。換句話說,使用者必須按下按鈕,網頁應用程式才會實際發出聲響。

playButton.addEventListener('pointerup', function(event) {
    let audio = document.querySelector('audio');

    // User interacted with the page. Let's play audio...
    audio.play()
    .then(_ => { /* Set up media session... */ })
    .catch(error => { console.log(error) });
});

如果您不想在第一次互動後立即播放音訊,建議您使用音訊元素的 load() 方法。這是瀏覽器追蹤使用者是否與元素互動的方式之一。請注意,這也可能有助於順暢播放,因為內容已載入。

let audio = document.querySelector('audio');

welcomeButton.addEventListener('pointerup', function(event) {
  // User interacted with the page. Let's load audio...
  <strong>audio.load()</strong>
  .then(_ => { /* Show play button for instance... */ })
  .catch(error => { console.log(error) });
});

// Later...
playButton.addEventListener('pointerup', function(event) {
  <strong>audio.play()</strong>
  .then(_ => { /* Set up media session... */ })
  .catch(error => { console.log(error) });
});

自訂通知

當網頁應用程式播放音訊時,您會在通知匣中看到媒體通知。在 Android 上,Chrome 會盡可能使用文件的標題和可找到的最大圖示圖片,顯示適當的資訊。

不使用媒體工作階段
不使用媒體工作階段
使用媒體工作階段
使用媒體工作階段

設定中繼資料

讓我們來看看如何使用 Media Session API 設定一些媒體工作階段中繼資料 (例如標題、藝人、專輯名稱和圖片),進而自訂這項媒體通知。

// When audio starts playing...
if ('mediaSession' in navigator) {

    navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Never Gonna Give You Up',
    artist: 'Rick Astley',
    album: 'Whenever You Need Somebody',
    artwork: [
        { src: 'https://dummyimage.com/96x96',   sizes: '96x96',   type: 'image/png' },
        { src: 'https://dummyimage.com/128x128', sizes: '128x128', type: 'image/png' },
        { src: 'https://dummyimage.com/192x192', sizes: '192x192', type: 'image/png' },
        { src: 'https://dummyimage.com/256x256', sizes: '256x256', type: 'image/png' },
        { src: 'https://dummyimage.com/384x384', sizes: '384x384', type: 'image/png' },
        { src: 'https://dummyimage.com/512x512', sizes: '512x512', type: 'image/png' },
    ]
    });
}

播放完成後,您不必「釋放」媒體工作階段,因為通知會自動消失。請注意,系統會在任何播放作業開始時使用目前的 navigator.mediaSession.metadata。因此,您需要更新該資訊,確保媒體通知一律顯示相關資訊。

上一首 / 下一首

如果您的網頁應用程式提供播放清單,您可能會想讓使用者直接透過媒體通知瀏覽播放清單,並顯示「上一曲」和「下一曲」圖示。

let audio = document.createElement('audio');

let playlist = ['audio1.mp3', 'audio2.mp3', 'audio3.mp3'];
let index = 0;

navigator.mediaSession.setActionHandler('previoustrack', function() {
    // User clicked "Previous Track" media notification icon.
    index = (index - 1 + playlist.length) % playlist.length;
    playAudio();
});

navigator.mediaSession.setActionHandler('nexttrack', function() {
    // User clicked "Next Track" media notification icon.
    index = (index + 1) % playlist.length;
    playAudio();
});

function playAudio() {
    audio.src = playlist[index];
    audio.play()
    .then(_ => { /* Set up media session... */ })
    .catch(error => { console.log(error); });
}

playButton.addEventListener('pointerup', function(event) {
    playAudio();
});

請注意,媒體操作處理常式會保留。這與事件事件監聽器模式非常相似,但處理事件代表瀏覽器會停止執行任何預設行為,並將此做為網頁應用程式支援媒體動作的信號。因此,除非您設定適當的動作處理常式,否則系統不會顯示媒體動作控制項。

順帶一提,取消設定媒體操作處理常式,就像將其指派給 null 一樣簡單。

倒轉 / 快轉

如要控制略過的時間長度,您可以使用 Media Session API 顯示「Seek Backward」和「Seek Forward」媒體通知圖示。

let skipTime = 10; // Time to skip in seconds

navigator.mediaSession.setActionHandler('seekbackward', function() {
    // User clicked "Seek Backward" media notification icon.
    audio.currentTime = Math.max(audio.currentTime - skipTime, 0);
});

navigator.mediaSession.setActionHandler('seekforward', function() {
    // User clicked "Seek Forward" media notification icon.
    audio.currentTime = Math.min(audio.currentTime + skipTime, audio.duration);
});

播放 / 暫停

「播放/暫停」圖示一律會顯示在媒體通知中,且相關事件會由瀏覽器自動處理。如果出於某些原因,預設行為無法運作,您仍可處理「播放」和「暫停」媒體事件。

navigator.mediaSession.setActionHandler('play', function() {
    // User clicked "Play" media notification icon.
    // Do something more than just playing current audio...
});

navigator.mediaSession.setActionHandler('pause', function() {
    // User clicked "Pause" media notification icon.
    // Do something more than just pausing current audio...
});

隨處顯示通知

Media Session API 的優點是,媒體中繼資料和控制項不只會顯示在通知列中。媒體通知會自動同步至任何已配對的可穿戴式裝置。這類通知也會顯示在螢幕鎖定畫面上。

螢幕鎖定
螢幕鎖定畫面 - 相片 由 Michael Alø-Nielsen / CC BY 2.0
Wear 通知
Wear 通知

讓應用程式在離線狀態下正常運作

我知道你現在在想什麼。服務工作者救星出動!

沒錯,但首先,請務必勾選此核對清單中的所有項目

  • 所有媒體和圖片檔案都會搭配適當的 Cache-Control HTTP 標頭提供。這樣一來,瀏覽器就能快取及重複使用先前擷取的資源。請參閱快取檢查清單
  • 請確認所有媒體和圖片檔案都會搭配 Allow-Control-Allow-Origin: * HTTP 標頭放送。這樣一來,第三方網頁應用程式就能從網路伺服器擷取及使用 HTTP 回應。

Service worker 快取策略

針對媒體檔案,我建議採用 Jake Archibald 所示的簡單「快取,改用網路」策略。

不過,針對圖片,我會更具體一點,選擇下列做法:

  • If 圖片已在快取中,請從快取中提供
  • Else 從網路擷取圖片
    • If 擷取成功,請將網路圖片加入快取並放送
    • Else 從快取提供備用圖片

這樣一來,即使瀏覽器無法擷取媒體通知,也能顯示精美的圖片圖示。實作方式如下:

const FALLBACK_ARTWORK_URL = 'fallbackArtwork.png';

addEventListener('install', event => {
    self.skipWaiting();
    event.waitUntil(initArtworkCache());
});

function initArtworkCache() {
    caches.open('artwork-cache-v1')
    .then(cache => cache.add(FALLBACK_ARTWORK_URL));
}

addEventListener('fetch', event => {
    if (/artwork-[0-9]+\.png$/.test(event.request.url)) {
    event.respondWith(handleFetchArtwork(event.request));
    }
});

function handleFetchArtwork(request) {
    // Return cache request if it's in the cache already, otherwise fetch
    // network artwork.
    return getCacheArtwork(request)
    .then(cacheResponse => cacheResponse || getNetworkArtwork(request));
}

function getCacheArtwork(request) {
    return caches.open('artwork-cache-v1')
    .then(cache => cache.match(request));
}

function getNetworkArtwork(request) {
    // Fetch network artwork.
    return fetch(request)
    .then(networkResponse => {
    if (networkResponse.status !== 200) {
        return Promise.reject('Network artwork response is not valid');
    }
    // Add artwork to the cache for later use and return network response.
    addArtworkToCache(request, networkResponse.clone())
    return networkResponse;
    })
    .catch(error => {
    // Return cached fallback artwork.
    return getCacheArtwork(new Request(FALLBACK_ARTWORK_URL))
    });
}

function addArtworkToCache(request, response) {
    return caches.open('artwork-cache-v1')
    .then(cache => cache.put(request, response));
}

讓使用者控制快取

當使用者使用您的網路應用程式消費內容時,媒體和圖片檔案可能會佔用大量裝置空間。您有責任顯示已使用的快取數量,並讓使用者清除快取。幸好,透過 Cache API 就能輕鬆完成這項作業。

// Here's how I'd compute how much cache is used by artwork files...
caches.open('artwork-cache-v1')
.then(cache => cache.matchAll())
.then(responses => {
    let cacheSize = 0;
    let blobQueue = Promise.resolve();

    responses.forEach(response => {
    let responseSize = response.headers.get('content-length');
    if (responseSize) {
        // Use content-length HTTP header when possible.
        cacheSize += Number(responseSize);
    } else {
        // Otherwise, use the uncompressed blob size.
        blobQueue = blobQueue.then(_ => response.blob())
            .then(blob => { cacheSize += blob.size; blob.close(); });
    }
    });

    return blobQueue.then(_ => {
    console.log('Artwork cache is about ' + cacheSize + ' Bytes.');
    });
})
.catch(error => { console.log(error); });

// And here's how to delete some artwork files...
const artworkFilesToDelete = ['artwork1.png', 'artwork2.png', 'artwork3.png'];

caches.open('artwork-cache-v1')
.then(cache => Promise.all(artworkFilesToDelete.map(artwork => cache.delete(artwork))))
.catch(error => { console.log(error); });

實作注意事項

  • 只有在媒體檔案的時間長度至少 5 秒時,Chrome 適用於 Android 的應用程式才會要求「完整」音訊專注功能,以便顯示媒體通知。
  • 通知圖片支援 blob 網址和資料網址。
  • 如果未定義圖片,且有適當大小的圖示圖片,媒體通知會使用該圖片。
  • Android 版 Chrome 的通知圖片大小為 512x512低階裝置則為 256x256
  • 使用 audio.src = '' 關閉媒體通知。
  • 由於 Web Audio API 基於歷史原因不會要求 Android Audio Focus,因此要讓它與 Media Session API 搭配運作,唯一的方法就是將 <audio> 元素掛鉤為 Web Audio API 的輸入來源。希望在近期,由 Google 提出的 Web AudioFocus API 能改善這個情況。
  • 只有在媒體資源的時間點與媒體工作階段呼叫相同時,媒體工作階段呼叫才會影響媒體通知。請參閱下方程式碼片段。
<iframe id="iframe">
  <audio>...</audio>
</iframe>
<script>
  iframe.contentWindow.navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Never Gonna Give You Up',
    ...
  });
</script>

支援

在撰寫本文時,Android 版 Chrome 是唯一支援 Media Session API 的平台。如要進一步瞭解瀏覽器導入狀態的最新資訊,請前往 Chrome 平台狀態

範例與示範

請參閱官方 Chrome Media Session 範例,其中包含 Blender FoundationJan Morgenstern 的作品

資源

媒體工作階段規格:wicg.github.io/mediasession

規格問題:github.com/WICG/mediasession/issues

Chrome 錯誤:crbug.com