Media Controls Beyond the Tab

When audio is playing in a browser tab, gaining quick control without hunting through dozens of open tabs can be surprisingly difficult. System-wide mute often feels like the only reliable option, and it kills all audio rather than just the offending track. The Media Session API solves this problem by exposing playback controls beyond the browser tab itself.

Browsers that implement the API surface media controls in locations like the notification shade on mobile, the media hub on desktop, and paired wearables. The API also enables hardware media keys and voice assistant integrations such as Siri, Google Assistant, and Alexa to drive playback.

Two Core Interfaces

The API is built around two primary interfaces: MediaMetadata and MediaSession. The former supplies descriptive data about the currently playing media — title, artist, album, and artwork. The latter handles transport actions like play, pause, seeking, and track changes.

As with any browser feature, feature detection is prudent before relying on the API:

if ('mediaSession' in navigator) {
  // Our media session api that lets us seek to the beginning of Kendrick Lamar's "Alright"
}

Describing the Media

To construct a new metadata object, call the MediaMetadata.MediaMetadata() constructor. The resulting instance supports these properties:

  • MediaMetadata.title — the playing media's title
  • MediaMetadata.artist — the artist or group name
  • MediaMetadata.album — the containing album
  • MediaMetadata.artwork — an array of image objects

The artwork property holds an array of MediaImage objects, each containing a src URL, a sizes field describing dimensions so the browser need not scale, and a type MIME string. The following creates metadata for Kendrick Lamar's "Alright" from To Pimp a Butterfly:

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

Handling Actions

The MediaSession interface exposes a set of user-invoked actions through the MediaSessionAction enumeration type: play, pause, previoustrack, nexttrack, seekbackward, seekforward, seekto, stop, and skipad. Registering support for an action involves calling setActionHandler() on the session object with the action string and a callback function.

Play and pause handlers are straightforward:

let alright = new HTMLAudioElement();

if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('play', () => {
    alright.play();
  });
  navigator.mediaSession.setActionHandler('pause', () => {
    alright.pause();
  });
}

Track navigation requires updating the current track when the previous or next action fires:

let u = new HTMLAudioElement();
let forSaleInterlude = new HTMLAudioElement();

if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('previoustrack', () => {
    u.play();
  });
  navigator.mediaSession.setActionHandler('nexttrack', () => {
    forSaleInterlude.play();
  });
}

Backward and forward seeking rely on the MediaSessionActionDetails dictionary, which optionally provides a seekOffset value. When that property absent — user agents vary — fall back to a sensible default such as 10 seconds:

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

Absolute seeking to a specific position also uses MediaSessionActionDetails. The required seekTime property holds the destination, while the optional fastSeek flag indicates whether the seek is rapid, as in fast-forward or rewind:

if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('seekto', (details) => {
    if (details.fastSeek && 'fastSeek' in alright) {
      alright.fastSeek(details.seekTime);
      return;
    }
    alright.currentTime = details.seekTime;
  });
}

Playback can also be halted entirely using the stop handler:

if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('stop', () => {
    alright.pause();
    alright.currentTime = 0;
  });
} 

The skipad action is available for systems that deliver advertisements, letting listeners skip over them to resume playback.

Position State and Permissions

Whenever playback position changes — through playing, seeking, or rate adjustment — the state must be synchronized with the browser's media controls using the setPositionState() method:

if ('mediaSession' in navigator) {
  navigator.mediaSession.setPositionState({
    duration: alright.duration,
    playbackRate: alright.playbackRate,
    position: alright.currentTime
  });
}

Because not every browser will support each action, it is wise to register handlers inside a try...catch block:

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

Combining all the pieces yields a complete integration:

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

Testing the API

A demo is available that implements six different actions:

On a mobile device, opening the demo surfaces the notifications area with playback options. Paired smart watches reflect similar controls. On desktop Chrome, the media hub displays the active session including multiple tracks for testing navigation actions. For any future projects with media playback, implementing this API gives users the control they expect without forcing them back into the originating tab.