假设这样一个场景:您在一个打开的浏览器标签页中播放了一首很棒的 Kendrick Lamar 的歌曲。您很喜欢这首歌,但这时有人走进了您的房间,您需要暂停它。哪个标签页在播放?浏览器试图稍微帮助您解决这个问题。您可能可以静音整个系统音频。但如果能够在不一定要找到该标签页的情况下,直接控制音频播放,岂不是更好?
媒体会话 API 使这成为可能。它允许用户在浏览器标签页之外控制媒体播放。如果实现,它将可以在设备上的多个位置使用,包括
- 许多移动设备上的通知区域,
- 其他可穿戴设备,以及
- 许多桌面设备的媒体中心区域。
此外,媒体会话 API 允许我们使用媒体键和语音助手(如 Siri、Google Assistant、Bixby 或 Alexa)来控制媒体播放。

媒体会话 API
媒体会话 API 主要由以下两个接口组成:
MediaMetadata
MediaSession
MediaMetadata
接口提供有关正在播放的媒体的数据。它负责告知我们媒体的标题、专辑、封面和艺术家(在本例中为 Kendrick Lamar)。MediaSession
接口负责媒体播放功能。
在我们深入探讨主题之前,我们需要了解特征检测。在实现功能之前检查浏览器是否支持该功能是一个好习惯。要检查浏览器是否支持媒体会话 API,我们需要在 JavaScript 文件中包含以下内容:
if ('mediaSession' in navigator) {
// Our media session api that lets us seek to the beginning of Kendrick Lamar's "Alright"
}
MediaMetadata 接口
构造函数 MediaMetadata.MediaMetadata()
创建一个新的 MediaMetadata
对象。创建后,我们可以添加以下属性:
MediaMetadata.title
设置或获取正在播放的媒体的标题。MediaMetadata.artist
设置或获取正在播放的媒体的艺术家或乐队的名称。MediaMetadata.album
设置或获取包含正在播放的媒体的专辑名称。MediaMetadata.artwork
设置或获取与正在播放的媒体相关的图像数组。
MediaMetadata
对象的 artwork
属性的值是一个 MediaImage
对象的数组。MediaImage
对象包含描述与媒体关联的图像的详细信息。这些对象具有以下三个属性:
src
:图像的 URLsizes
:指示图像的大小,以便无需缩放单个图像。type
:图像的 MIME 类型
让我们为 Kendrick Lamar 的专辑 To Pimp a Butterfly 中的歌曲“Alright”创建一个 MediaMetadata
对象。
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: 'Alright',
artist: 'Kendrick Lamar',
album: 'To Pimp A Butterfly',
artwork: [
{ src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/96x96', sizes: '96x96', type: 'image/png' },
{ src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/128x128', sizes: '128x128', type: 'image/png' },
// More sizes, like 192x192, 256x256, 384x384, and 512x512
]
});
}
MediaSession 接口
如前所述,它允许用户控制媒体的播放。我们可以通过此接口对正在播放的媒体执行以下操作:
play
:播放媒体pause
:暂停媒体previoustrack
:切换到上一曲目nexttrack
:切换到下一曲目seekbackward
:从当前位置后退几秒钟seekforward
:从当前位置快进几秒钟seekto
:从当前位置跳转到指定时间点stop
:停止媒体播放skipad
:跳过正在播放的广告(如果有)
MediaSessionAction
枚举类型将这些操作作为字符串类型提供。要支持任何这些操作,我们必须使用 MediaSession
的 setActionHandler()
方法为该操作定义一个处理程序。该方法接受操作和一个回调函数,当用户调用该操作时,该回调函数会被调用。让我们稍微深入了解一下,以便更好地理解它。
要为 play
和 pause
操作设置处理程序,我们需要在 JavaScript 文件中包含以下内容:
let alright = new HTMLAudioElement();
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('play', () => {
alright.play();
});
navigator.mediaSession.setActionHandler('pause', () => {
alright.pause();
});
}
在这里,当用户通过媒体界面播放时,我们将曲目设置为播放;当用户暂停时,将其设置为暂停。
对于 previoustrack
和 nexttrack
操作,我们需要包含以下内容:
let u = new HTMLAudioElement();
let forSaleInterlude = new HTMLAudioElement();
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('previoustrack', () => {
u.play();
});
navigator.mediaSession.setActionHandler('nexttrack', () => {
forSaleInterlude.play();
});
}
如果您不是 Kendrick Lamar 的粉丝,这可能不太容易理解,但希望您能掌握要点。当用户想要播放上一曲目时,我们将上一曲目设置为播放;当是下一曲目时,则为下一曲目。
要实现 seekbackward
和 seekforward
操作,我们需要包含以下内容:
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('seekbackward', (details) => {
alright.currentTime = alright.currentTime - (details.seekOffset || 10);
});
navigator.mediaSession.setActionHandler('seekforward', (details) => {
alright.currentTime = alright.currentTime + (details.seekOffset || 10);
});
}
鉴于我认为这些内容都不容易理解,我想对 seekbackward
和 seekforward
操作进行简要说明。这两个操作的处理程序 seekbackward
和 seekforward
在用户想要后退或快进几秒钟时被触发,顾名思义。MediaSessionActionDetails
字典在 seekOffset
属性中提供“几秒钟”的值。但是,seekOffset
属性并不总是存在,因为并非所有用户代理的行为都相同。当它不存在时,我们应该将曲目设置为后退或快进“几秒钟”,这对我们来说是有意义的。因此,我们使用 10 秒,因为它相当长。简而言之,如果提供了 seekOffset
秒,我们将曲目设置为跳转该时间;如果没有提供,则跳转 10 秒。
要向我们的媒体会话 API 添加 seekto
功能,我们需要包含以下代码片段:
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('seekto', (details) => {
if (details.fastSeek && 'fastSeek' in alright) {
alright.fastSeek(details.seekTime);
return;
}
alright.currentTime = details.seekTime;
});
}
在这里,MediaSessionActionDetails
字典提供 fastSeek
和 seekTime
属性。fastSeek
基本上是快速执行的跳转(如快进或倒带),而 seekTime
是曲目应该跳转到的时间。虽然 fastSeek
是一个可选属性,但对于 seekto
操作处理程序,MediaSessionActionDetails
字典始终提供 seekTime
属性。因此,从根本上讲,当属性可用且用户快速跳转时,我们将曲目 fastSeek
设置为 seekTime
;当用户只是跳转到指定时间时,我们将其设置为 seekTime
。
虽然我不知道为什么有人会想停止 Kendrick 的歌曲,但描述 MediaSession
接口的 stop
操作处理程序也不会造成什么伤害:
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('stop', () => {
alright.pause();
alright.currentTime = 0;
});
}
当广告正在播放且用户想要跳过它以便继续收听 Kendrick Lamar 的 “Alright” 歌曲时,用户会调用 skipad
(就像“跳过广告”而不是“跳过垫子”)操作处理程序。老实说,skipad
操作处理程序的完整细节超出了我对“媒体会话 API”的理解范围。因此,如果您真的想实现它,可能应该在阅读完本文后自己查找相关信息。
总结
我们需要注意一件事。每当用户播放曲目、跳转或更改播放速度时,我们都应该更新媒体会话 API 提供的界面上的位置状态。我们用来实现此目的的是 mediaSession
对象的 setPositionState()
方法,如下所示:
if ('mediaSession' in navigator) {
navigator.mediaSession.setPositionState({
duration: alright.duration,
playbackRate: alright.playbackRate,
position: alright.currentTime
});
}
此外,我想提醒您,并非所有用户的浏览器都支持所有操作。因此,建议在 try...catch
块中设置操作处理程序,如下所示:
const actionsAndHandlers = [
['play', () => { /*...*/ }],
['pause', () => { /*...*/ }],
['previoustrack', () => { /*...*/ }],
['nexttrack', () => { /*...*/ }],
['seekbackward', (details) => { /*...*/ }],
['seekforward', (details) => { /*...*/ }],
['seekto', (details) => { /*...*/ }],
['stop', () => { /*...*/ }]
]
for (const [action, handler] of actionsAndHandlers) {
try {
navigator.mediaSession.setActionHandler(action, handler);
} catch (error) {
console.log(`The media session action, ${action}, is not supported`);
}
}
将我们所做的一切整合在一起,我们将得到以下内容:
let alright = new HTMLAudioElement();
let u = new HTMLAudioElement();
let forSaleInterlude = new HTMLAudioElement();
const updatePositionState = () => {
navigator.mediaSession.setPositionState({
duration: alright.duration,
playbackRate: alright.playbackRate,
position: alright.currentTime
});
}
const actionsAndHandlers = [
['play', () => {
alright.play();
updatePositionState();
}],
['pause', () => { alright.pause(); }],
['previoustrack', () => { u.play(); }],
['nexttrack', () => { forSaleInterlude.play(); }],
['seekbackward', (details) => {
alright.currentTime = alright.currentTime - (details.seekOffset || 10);
updatePositionState();
}],
['seekforward', (details) => {
alright.currentTime = alright.currentTime + (details.seekOffset || 10);
updatePositionState();
}],
['seekto', (details) => {
if (details.fastSeek && 'fastSeek' in alright) {
alright.fastSeek(details.seekTime);
updatePositionState();
return;
}
alright.currentTime = details.seekTime;
updatePositionState();
}],
['stop', () => {
alright.pause();
alright.currentTime = 0;
}],
]
if ( 'mediaSession' in navigator ) {
navigator.mediaSession.metadata = new MediaMetadata({
title: 'Alright',
artist: 'Kendrick Lamar',
album: 'To Pimp A Butterfly',
artwork: [
{ src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/96x96', sizes: '96x96', type: 'image/png' },
{ src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/128x128', sizes: '128x128', type: 'image/png' },
// More sizes, like 192x192, 256x256, 384x384, and 512x512
]
});
for (const [action, handler] of actionsAndHandlers) {
try {
navigator.mediaSession.setActionHandler(action, handler);
} catch (error) {
console.log(`The media session action, ${action}, is not supported`);
}
}
}
以下是该 API 的演示:
我实现了六个操作。请在您空闲时间尝试其余操作。
如果您在移动设备上查看此 Pen,请注意它如何显示在您的通知区域中。
太棒了!希望有一天能实现它,谢谢先生
不客气!
它远不止于此!有些耳机或耳麦具有自动暂停功能,可以向其连接的任何蓝牙源发送播放/暂停信号。我非常高兴终于能够通过摘下耳机或在有人开始与我互动时用手掌触摸它来暂停任何 YouTube 视频或 Bootcamp 播放列表。
我必须承认,它甚至适用于一些我认为没有真正实现它的旧网络播放器。也许,因为此类网站使用标准 HTML 音频播放,浏览器能够控制它。
无论如何,非常感谢您的这篇文章。
我实际上在引言中提到过媒体键(如耳机上的播放/暂停键)受 API 支持。YouTube 也实现了该 API。
不客气。
我有一个媒体键,我不知道它的名字,但每当我点击它时,youtube.com 就会打开 O_o
如何获取支持的媒体键列表?
我建议您查找包含媒体键的设备的规格。
嗨,Media Session API 在 Android 应用中有效吗?
请做一个教程或演示。
是的,该 API 可以实现到 Android 应用中,但这超出了我的范围。
您可以在网上找到相关的教程。
是否可以在没有`` 或 `` 元素的情况下使用`MediaMetadata`?
我想显示来自 SoundCloud API 的音频元数据——因此不提供`source`——但据我了解,使用`MediaMetadata` 需要播放这两个元素之一,是吗?
有没有什么技巧可以规避此要求?
Kael,你只需在 Javascript 中执行 new Audio() 或 new Video()。如果你想隐藏源以防止人们下载实际文件,你可以将其放入内存并从那里加载。一些技巧,如果你从服务器发送文件路径,是在初始化期间加载到内存中并从属性中删除值,以便人们无法使用检查器查看路径,除非他们知道你在使用此技巧并在你的初始化执行之前插入断点
在实现此功能时,我意识到有时如果您向 Javascript 添加代码并刷新(即使禁用缓存),Chrome 上的播放器由于某种原因无法正常加载。您需要打开一个新标签页并再次加载页面。我用不同人的多段代码尝试过,结果都一样,所以我认为这是 Chrome 的问题。
我意识到的第二件事是 MediaSession 没有音量控制。在 iOS 上,有一个音量条,因此它与网络上的音量不匹配
let alright = new HTMLAudioElement();
抛出`Uncaught TypeError: Illegal constructor`