# Media Session API

> navigator.mediaSession 如何设置元数据与动作处理函数，供操作系统级媒体控件使用，以及 Chrome、Firefox、Safari 的支持版本。

**一句话：** 根据 MDN 的说明，Media Session API "提供了一种自定义媒体通知的方式"——它让页面
描述当前正在播放的内容，并接收来自设备物理或屏幕媒体控件的事件，与页面自身已有的
播放控件并存。

## 这是什么

入口是 `navigator.mediaSession`，一个 `MediaSession` 对象。根据 MDN 的
`setActionHandler()` 参考文档，它的动作"让 Web 应用能够在用户操作设备内置的物理或屏幕
媒体控件（例如播放、停止或快进/快退按钮）时收到通知"；根据 MDN 的 `metadata` 参考文档，
`MediaSession.metadata` 持有一个 `MediaMetadata` 对象，为设备自身的媒体控制界面提供
当前播放内容的描述信息。

## 支持情况

根据 MDN 的 browser-compat-data，`MediaSession` 接口（及其 `setActionHandler()` 方法）
自 Chrome 73 起受支持，自 Safari 15 起受支持，自 Firefox 82 起受支持。Chrome for
Android 自 57 起支持，但 Firefox for Android 只是部分实现——同一份数据指出 Firefox
for Android "暴露了该 API，但没有提供对应的用户可见媒体控制界面"。该数据将 Android
WebView 记录为不支持（`version_added: false`）。

## 如何使用

设置 `metadata` 为一个 `MediaMetadata` 对象，然后为页面能够响应的动作注册处理函数：

```js
const playlist = [
  { title: "Unforgettable", artist: "Nat King Cole", album: "The Ultimate Collection (Remastered)" },
  { title: "L-O-V-E", artist: "Nat King Cole", album: "The Ultimate Collection (Remastered)" },
];
let currentTrackIndex = 0;
const audioElement = document.querySelector("audio") ?? document.createElement("audio");

function loadTrack(index) {
  currentTrackIndex = index;
  const track = playlist[currentTrackIndex];
  audioElement.src = `/audio/${track.title}.mp3`;
  navigator.mediaSession.metadata = new MediaMetadata({
    title: track.title,
    artist: track.artist,
    album: track.album,
    artwork: [{ src: "/art/unforgettable-96.png", sizes: "96x96", type: "image/png" }],
  });
}

function playPreviousTrack() {
  if (currentTrackIndex > 0) {
    loadTrack(currentTrackIndex - 1);
    audioElement.play();
  }
}

function playNextTrack() {
  if (currentTrackIndex < playlist.length - 1) {
    loadTrack(currentTrackIndex + 1);
    audioElement.play();
  }
}

loadTrack(0);

const actionHandlers = {
  play: () => audioElement.play(),
  pause: () => audioElement.pause(),
  previoustrack: playPreviousTrack,
  nexttrack: playNextTrack,
};

for (const [action, handler] of Object.entries(actionHandlers)) {
  try {
    navigator.mediaSession.setActionHandler(action, handler);
  } catch {
    // 该浏览器未实现这个媒体会话动作——跳过。
  }
}
```

根据 MDN 的 `setActionHandler()` 参考文档，将 `null` 作为回调传入会移除此前为该动作
设置的处理函数，例如 `navigator.mediaSession.setActionHandler("nexttrack", null)`。

## 运行时检测与回退

在读写之前先对 `navigator` 上的 `mediaSession` 做特性检测，不支持时回退为显示页面自身的
播放/暂停/快进控件，将其作为唯一的控制方式。下面的示例自行创建了这个回退元素
（一个原生的 `<audio controls>`），因此直接粘贴运行即可生效，而不是假设页面上已经存在
从未添加过的标记：

```js
function supportsMediaSession() {
  return "mediaSession" in navigator;
}

const playerControls = document.querySelector("#player-controls") ?? createPlayerControls();

if (supportsMediaSession()) {
  navigator.mediaSession.metadata = new MediaMetadata({ title: "Unforgettable" });
} else {
  // 此处不支持 Media Session——依赖页面自身的播放/暂停/快进控件，
  // 而不是操作系统级别的媒体控件。
  showOnPageTransportControls();
}

function createPlayerControls() {
  const audio = document.createElement("audio");
  audio.id = "player-controls";
  audio.controls = true;
  audio.src = "/audio/unforgettable.mp3";
  audio.hidden = true;
  document.body.appendChild(audio);
  return audio;
}

function showOnPageTransportControls() {
  playerControls.hidden = false;
}
```

## 实用清单

- 根据 MDN 的兼容性数据，Firefox for Android "暴露了该 API，但没有提供对应的用户可见
  媒体控制界面"——在该平台上不要因为 `setActionHandler()` 没有抛出异常就假定用户能
  实际接触到已设置的处理函数。
- 同一份数据将 Android WebView 记录为不支持——嵌入在 WebView 中的应用仍需要页面自身的
  回退控件。
- 根据 MDN 的 `setActionHandler()` 参考文档，各个动作字符串（例如 `skipad`、
  `previousslide`、`nextslide`）的支持情况因浏览器而异，对某浏览器未实现的动作调用
  该方法可能会抛出异常——MDN 自己的示例代码将每次 `setActionHandler()` 调用都包裹在
  独立的 `try...catch` 中，避免一个不受支持的动作影响其余设置。
- 根据 MDN 的 `metadata` 参考文档，`MediaSession.metadata` 在页面设置之前为 `null`——
  应做防御性读取，而不要假定 `MediaMetadata` 对象已经存在。

## 相关参考

- [Document Picture-in-Picture：置顶窗口](/zh/reference/capabilities/document-picture-in-picture/)
- [Screen Wake Lock API](/zh/reference/capabilities/wake-lock/)