借助全新的 Media Session API,您现在可以为 Web 应用所播放的媒体提供元数据,从而自定义媒体通知。另外,您也可以利用此 API 处理与媒体相关的事件,例如定位播放位置或更改轨道(这些实践可能来自通知或媒体键)。听起来不错吧?试用官方的媒体会话示例。
Chrome 57 支持 Media Session API(2017 年 2 月发布 Beta 版,2017 年 3 月发布稳定版)。
Gimme what I want
您已经了解 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 for Android 上的音频元素已停用 autoplay
,这意味着我们必须使用音频元素的 play()
方法。此方法必须由用户手势(例如触摸或点击鼠标)触发。这意味着监听 pointerup
、click
和 touchend
事件。换句话说,用户必须点击某个按钮,您的 Web 应用才能实际发出声音。
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);
});
播放 / 暂停
“播放/暂停”图标始终显示在媒体通知中,并且相关事件由浏览器自动处理。如果默认行为因某种原因不起作用,您仍然可以处理“播放”和“暂停”媒体事件。
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 的一大亮点是,通知栏并非媒体元数据和控件唯一的显示位置。媒体通知会自动同步到任何已配对的穿戴式设备。它还会显示在锁定屏幕上。
确保其能够在离线状态下正常播放
我知道您现在在想什么。服务工件大显身手!
没错,但首先,您需要确保已勾选此核对清单中的所有项目:
- 所有媒体和海报图片文件均附带适当的
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 for Android 才会请求“完整”音频焦点以显示媒体通知。
- 通知海报图片支持 blob 网址和数据网址。
- 如果未定义任何海报图片,并且有合适大小的图标图片,媒体通知将使用该图标图片。
- Android 版 Chrome 中的通知海报图片大小为
512x512
。对于低端设备,该值为256x256
。 - 使用
audio.src = ''
关闭媒体通知。 - 由于历史原因,Web Audio API 不会请求 Android 音频焦点,因此要让它与 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 for Android 是唯一支持 Media Session API 的平台。如需了解有关浏览器实现状态的最新信息,请访问 Chrome 平台状态。
示例和演示
查看我们的官方 Chrome 媒体会话示例,其中包含 Blender Foundation 和 Jan Morgenstern 的作品。
资源
Media Session 规范: wicg.github.io/mediasession
规范问题: github.com/WICG/mediasession/issues
Chrome bug: crbug.com