# Media Session API

> How navigator.mediaSession sets metadata and action handlers for OS-level media controls, with Chrome, Firefox, and Safari support versions.

**In one line:** the Media Session API "provides a way to customize media notifications,"
per MDN — it lets a page describe what is playing and receive events from a device's
physical or onscreen media controls, alongside whatever on-page playback controls the
page already has.

## What it is

The entry point is `navigator.mediaSession`, a `MediaSession` object. Per MDN's
`setActionHandler()` reference, its actions "let a web app receive notifications when
the user engages a device's built-in physical or onscreen media controls, such as play,
stop, or seek buttons," and per MDN's `metadata` reference, `MediaSession.metadata` holds
a `MediaMetadata` object that provides descriptive information about the currently
playing media for the device's own media control UI.

## Where it is supported

Per MDN's browser-compat-data, the `MediaSession` interface (and its `setActionHandler()`
method) is supported in Chrome from version 73, in Safari from version 15, and in Firefox
from version 82. Chrome for Android has supported it from version 57, but Firefox for
Android is a partial implementation — the same data notes that Firefox for Android
"exposes the API, but does not provide a corresponding user-facing media control
interface." The data records Android WebView as unsupported (`version_added: false`).

## How to use it

Set `metadata` with a `MediaMetadata` object, then register handlers for the actions the
page can respond to:

```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 {
    // This browser doesn't implement the "action" media session action — skip it.
  }
}
```

Per MDN's `setActionHandler()` reference, passing `null` as the callback removes a
previously-set handler for that action, e.g.
`navigator.mediaSession.setActionHandler("nexttrack", null)`.

## How to detect it at runtime

Feature-detect `mediaSession` on `navigator` before reading or writing it, and fall back
to showing the page's own on-page playback controls as the only control surface. This
example creates that fallback element itself (a native `<audio controls>`) so it works
when pasted as-is, instead of assuming markup that was never added to the page:

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

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

if (supportsMediaSession()) {
  navigator.mediaSession.metadata = new MediaMetadata({ title: "Unforgettable" });
} else {
  // No Media Session support here — rely on the page's own play/pause/seek
  // controls instead of OS-level media controls.
  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;
}
```

## Practical checklist

- Per MDN's compat data, Firefox for Android "exposes the API, but does not provide a
  corresponding user-facing media control interface" — do not assume a set handler is
  reachable by the user on that platform just because `setActionHandler()` did not throw.
- Android WebView is recorded as unsupported in the same data — an app embedded in a
  WebView still needs the on-page fallback controls.
- Per MDN's `setActionHandler()` reference, support for individual action strings (e.g.
  `skipad`, `previousslide`, `nextslide`) varies by browser, and calling it with an action
  a given browser doesn't implement can throw — MDN's own examples wrap each
  `setActionHandler()` call in its own `try...catch` so one unsupported action doesn't
  break the rest of the session setup.
- Per MDN's `metadata` reference, `MediaSession.metadata` is `null` until the page sets
  it — read it defensively rather than assuming a `MediaMetadata` object is already
  present.

## Where to go next

- [Document Picture-in-Picture: always-on-top window](/reference/capabilities/document-picture-in-picture/)
- [Screen Wake Lock API](/reference/capabilities/wake-lock/)