自定义媒体通知和处理播放列表

François Beaufort
François Beaufort

借助全新的 Media Session API,您现在可以通过提供 Web 应用正在播放的媒体的元数据,自定义媒体通知。此外,您还可以处理媒体相关事件,例如可能来自通知或媒体键的跳转或轨道更改。听起来不错吧?欢迎试用官方媒体会话示例

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

媒体会话要点;
照片 ,作者: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>

如您所知,在 Chrome(Android 版)中,音频元素的 autoplay 处于停用状态,这意味着我们必须使用音频元素的 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) });
});

自定义通知

当您的 Web 应用正在播放音频时,通知栏中已经显示了媒体通知。在 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。因此,您需要对其进行更新,以确保始终在媒体通知中显示相关信息。

上一首 / 下一首

如果您的 Web 应用提供了播放列表,您可能需要使用一些“上一首”和“下一首”图标,允许用户直接从媒体通知中浏览播放列表。

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();
});

请注意,媒体操作处理程序将持久保存。这与事件监听器模式非常相似,不同之处在于处理事件意味着浏览器停止执行任何默认行为,并将其用作指示您的 Web 应用支持媒体操作的信号。因此,除非您设置正确的操作处理程序,否则媒体操作控件不会显示。

顺便提一下,取消设置媒体操作处理程序就像将其分配给 null 一样简单。

快退 / 快进

如果您想控制跳过的时间,可以使用 Media Session API 显示“向后跳转”和“快进”媒体通知图标。

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);
});

播放 / 暂停

媒体通知中始终显示“播放/暂停”图标,且浏览器会自动处理相关事件。如果由于某种原因无法触发默认行为,您仍可以处理“Play”和“Pause”媒体事件。

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 通知

打造离线播放体验

我知道你现在在想什么。这种情况下就轮到 Service Worker 了!

正确,但最重要的是,您需要确保此核对清单中的所有项目均已选中:

  • 所有媒体文件和海报图片文件都会带有相应的 Cache-Control HTTP 标头。这将允许浏览器缓存并重复使用之前提取的资源。请参阅缓存核对清单
  • 确保所有媒体文件和海报图片文件都带有 Allow-Control-Allow-Origin: * HTTP 标头。这将允许第三方 Web 应用从您的 Web 服务器提取和使用 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));
}

允许用户控制缓存

随着用户使用 Web 应用中的内容,媒体文件和海报图片文件可能会占用用户设备上的大量空间。您负责说明缓存已用多少,并让用户能够清除缓存。幸运的是,使用 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 网址和数据网址。
  • 如果未定义图片,并且存在所需尺寸的图标图片,媒体通知将使用该图片。
  • Chrome(Android 版)中的通知图片大小为 512x512。对于低端设备,此值为 256x256
  • 使用 audio.src = '' 关闭媒体通知。
  • 由于 Web Audio API 出于历史原因不会请求 Android Audio Focus,因此使其与 Media Session API 搭配使用的唯一方法是将 <audio> 元素作为输入源挂接到 Web Audio API。我们希望在不久的将来,提议的 Web AudioFocus API 会改善这种情况。
  • 仅当媒体通知来自与媒体资源相同的帧时,媒体会话调用才会影响媒体通知。请参阅以下代码段。
<iframe id="iframe">
  <audio>...</audio>
</iframe>
<script>
  iframe.contentWindow.navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Never Gonna Give You Up',
    ...
  });
</script>

支持

在撰写本文时,Chrome(Android 版)是唯一支持 Media Session API 的平台。如需了解有关浏览器实现状态的最新信息,请参阅 Chrome 平台状态

示例和演示

查看我们的 Chrome 媒体会话示例,其中介绍了 Blender FoundationJan Morgenstern 的工作

资源

媒体会话规范:wicg.github.io/mediasession

规范问题:github.com/WICG/mediasession/issues

Chrome Bug:crbug.com