Playing media beyond the tab
When audio or video plays in a browser, users don't have to stay on the page to control it. The Media Session API bridges that gap, letting web apps expose playback information and handle commands that arrive from outside the document itself. These entry points include the desktop media hub, mobile lock-screen notifications, hardware media keys, and even paired wearables. Through the MediaSession and MediaMetadata interfaces, developers can take charge of that experience instead of leaving it to browser defaults.
What once required returning to the originating tab can now be done from where the user already is. Consider a keyboard's "next track" button, a seek-backward icon on a locked phone, a media hub stop button for a noisy background tab, or a toggle-microphone control in a video call's Picture-in-Picture window. Media Session actions make each of those inputs resolvable by the web app.
Browser support for the core API spans Chrome, Edge, Firefox, and Safari, with slide-presentation actions available in Chromium-based browsers.
Beyond the default metadata
Browsers already show something while media plays — typically the document title and a grabbed icon. The API refines that with explicit MediaMetadata: a richer set including artist, album, and properly sized artwork. Chrome requests full audio focus before it will surface these notifications, and it only does so for media lasting at least five seconds so that short UI sounds don't trigger them.
// After media (video or audio) starts playing
await document.querySelector("video").play();
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://via.placeholder.com/96', sizes: '96x96', type: 'image/png' },
{ src: 'https://via.placeholder.com/128', sizes: '128x128', type: 'image/png' },
{ src: 'https://via.placeholder.com/192', sizes: '192x192', type: 'image/png' },
{ src: 'https://via.placeholder.com/256', sizes: '256x256', type: 'image/png' },
{ src: 'https://via.placeholder.com/384', sizes: '384x384', type: 'image/png' },
{ src: 'https://via.placeholder.com/512', sizes: '512x512', type: 'image/png' },
]
});
// TODO: Update playback state.
}
Notifications clear themselves when playback ends, and there's no release call to make. A new playback session does reuse the previous navigator.mediaSession.metadata, so it's worth updating that value when the source changes. A few practical details: artwork accepts blob and data URLs; Chrome for Android expects 512×512 images, or 256×256 on low-end devices; if no artwork is set, a suitably sized <link rel=icon> is used instead; and on macOS the "Now playing" widget reads the title attribute of the media element. For iframes, metadata must be set from within the embedded document.
<iframe id="iframe">
<video>...</video>
</iframe>
<script>
iframe.contentWindow.navigator.mediaSession.metadata = new MediaMetadata({
title: 'Never Gonna Give You Up',
...
});
</script>
The same metadata object can carry chapter markers, giving users a way to jump between sections of long-form content without opening the page.
navigator.mediaSession.metadata = new MediaMetadata({
// title, artist, album, artwork, ...
chapterInfo: [{
title: 'Chapter 1',
startTime: 0,
artwork: [
{ src: 'https://via.placeholder.com/128', sizes: '128x128', type: 'image/png' },
{ src: 'https://via.placeholder.com/512', sizes: '512x512', type: 'image/png' },
]
}, {
title: 'Chapter 2',
startTime: 42,
artwork: [
{ src: 'https://via.placeholder.com/128', sizes: '128x128', type: 'image/png' },
{ src: 'https://via.placeholder.com/512', sizes: '512x512', type: 'image/png' },
]
}]
});
Handling playback actions
Media session actions behave much like events: a website registers a handler on the MediaSession instance, and the browser invokes it when the user triggers the corresponding control from a headset, remote, keyboard, or notification. Because certain actions may not be supported everywhere, wrapping the setup in a try…catch block is a safe habit, and setting a handler to null removes it.
navigator.mediaSession.setActionHandler('nexttrack', () => {
// Play next track.
});
Unlike event listeners, a registered action does more than respond to input: it signals the browser that the feature is supported, which determines whether its control is shown at all. Playback events like seeking and buffering can make the browser mark the media as not playing. When overriding defaults, apps can force the correct icon by setting navigator.mediaSession.playbackState to "playing" or "paused".
const video = document.querySelector('video');
navigator.mediaSession.setActionHandler('play', async () => {
// Resume playback
await video.play();
});
navigator.mediaSession.setActionHandler('pause', () => {
// Pause active playback
video.pause();
});
video.addEventListener('play', () => {
navigator.mediaSession.playbackState = 'playing';
});
video.addEventListener('pause', () => {
navigator.mediaSession.playbackState = 'paused';
});
Track and seek controls
The usual transport actions are available. The "play" and "pause" actions handle resume and halt; "previoustrack" and "nexttrack" step through a playlist or restart the current item; and "stop" halts playback and allows app state cleanup. Since some devices don't expose every action, try-catch blocks are recommended around handler registrations.
navigator.mediaSession.setActionHandler('previoustrack', () => {
// Play previous track.
});
For "seekbackward" and "seekforward", the handler receives a seekOffset in seconds. When the offset is absent, the app should choose its own interval — typically somewhere in the 10–30 second range.
const video = document.querySelector('video');
const defaultSkipTime = 10; /* Time to skip in seconds by default */
navigator.mediaSession.setActionHandler('seekbackward', (details) => {
const skipTime = details.seekOffset || defaultSkipTime;
video.currentTime = Math.max(video.currentTime - skipTime, 0);
// TODO: Update playback state.
});
navigator.mediaSession.setActionHandler('seekforward', (details) => {
const skipTime = details.seekOffset || defaultSkipTime;
video.currentTime = Math.min(video.currentTime + skipTime, video.duration);
// TODO: Update playback state.
});
Seeking to an exact time is handled by "seekto", where the seekTime property provides the destination and fastSeek indicates whether the action is part of a rapid sequence (such as dragging a scrubber) rather than a final resting position.
const video = document.querySelector('video');
navigator.mediaSession.setActionHandler('seekto', (details) => {
if (details.fastSeek && 'fastSeek' in video) {
// Only use fast seek if supported.
video.fastSeek(details.seekTime);
return;
}
video.currentTime = details.seekTime;
// TODO: Update playback state.
});
Reflecting position state
Notifications can accurately show where playback stands by keeping the position state current. This state rates the media's playback rate, duration, and current position, and it must remain internally consistent: a positive duration, a position between zero and the duration, and a rate above zero.
const video = document.querySelector('video');
function updatePositionState() {
if ('setPositionState' in navigator.mediaSession) {
navigator.mediaSession.setPositionState({
duration: video.duration,
playbackRate: video.playbackRate,
position: video.currentTime,
});
}
}
// When video starts playing, update duration.
await video.play();
updatePositionState();
// When user wants to seek backward, update position.
navigator.mediaSession.setActionHandler('seekbackward', (details) => {
/* ... */
updatePositionState();
});
// When user wants to seek forward, update position.
navigator.mediaSession.setActionHandler('seekforward', (details) => {
/* ... */
updatePositionState();
});
// When user wants to seek to a specific time, update position.
navigator.mediaSession.setActionHandler('seekto', (details) => {
/* ... */
updatePositionState();
});
// When video playback rate changes, update position state.
video.addEventListener('ratechange', (event) => {
updatePositionState();
});
Apps can clear that state completely by setting navigator.mediaSession.setPositionState()'s argument to null.
Conference and presentation controls
Beyond music and video, the API extends outward into other immersive contexts. For video calls or slide decks shown in Picture-in-Picture, browsers can show dedicated controls — available only when the page declares support for them through the proper action handlers.
Video-conference actions add call-specific commands on top of transport ones. The "togglemicrophone" and "togglecamera" actions let users mute their input or disable the camera from the floating window. Each pairs with a companion method — setMicrophoneActive(isActive) and setCameraActive(isActive) — used to sync the UI indicator with which input is actually connected. The "hangup" action ends the call entirely.
let isMicrophoneActive = false;
navigator.mediaSession.setActionHandler('togglemicrophone', () => {
if (isMicrophoneActive) {
// Mute the microphone.
} else {
// Unmute the microphone.
}
isMicrophoneActive = !isMicrophoneActive;
navigator.mediaSession.setMicrophoneActive(isMicrophoneActive);
});
navigator.mediaSession.setActionHandler('hangup', () => {
// End the call.
});
For presenters, the "previousslide" and "nextslide" actions move through a deck from the Picture-in-Picture window, keeping an audience's flow intact when the presenter isn't on the control tab. These actions appear within Chromium's 111 release series.
navigator.mediaSession.setActionHandler('previousslide', () => {
// Show previous slide.
});
Going further
Interactive demonstrations of these features are available in the official Media Session samples collection, with examples built around open-source media from the Blender Foundation and Jan Morgenstern. The WICG specification and its issue tracker, alongside Chromium's bug database, are the resources for implementation questions.



