Media Session API is what makes a PWA media player feel native
Practical Media Session patterns for PWA media apps that keep playback synced with OS controls across lock screen and notifications.
Why media PWAs need Media Session integration
When users play audio or video in a PWA, they expect the same controls they get in native apps: lock screen playback, notification media controls, and headset button handling. The Media Session API makes this integration possible, but it requires the PWA to actively set and update media metadata as playback progresses.
Without Media Session, audio continues playing but the PWA leaves no trace in the OS control layer. When audio stops or users navigate away, they lose the standard pause/skip controls. In practice, this means the app doesn't feel "native" when it should.
Set up Media Session metadata once playback starts
Initialize the session as soon as media begins, not on page load:
const audio = new Audio('track.mp3');
audio.addEventListener('play', () => {
navigator.mediaSession.metadata = new MediaMetadata({
title: 'Track Name',
artist: 'Artist Name',
album: 'Album Name',
artwork: [
{ src: '/album-art-96.png', sizes: '96x96', type: 'image/png' },
{ src: '/album-art-256.png', sizes: '256x256', type: 'image/png' },
{ src: '/album-art-512.png', sizes: '512x512', type: 'image/png' }
]
});
navigator.mediaSession.setActionHandler('play', () => {
audio.play();
});
navigator.mediaSession.setActionHandler('pause', () => {
audio.pause();
});
navigator.mediaSession.setActionHandler('previoustrack', () => {
// Go to previous track
});
navigator.mediaSession.setActionHandler('nexttrack', () => {
// Go to next track
});
audio.play();
});Wait for the media element to start playing before setting metadata. If you set it too early, browsers may delay processing or ignore the session.
Update the seekable position continuously
Lock screen controls and progress bars can show a seekable range only if you update the Media Session playback state as audio plays:
function updateMediaSession() {
if ('setPositionState' in navigator.mediaSession) {
navigator.mediaSession.setPositionState({
duration: audio.duration,
playbackRate: audio.playbackRate,
position: audio.currentTime
});
}
}
audio.addEventListener('timeupdate', () => {
updateMediaSession();
});
// Also update when user seeks manually
audio.addEventListener('seeked', () => {
updateMediaSession();
});Debounce timeupdate calls if you notice performance issues—every second is usually sufficient for UI updates.
Tell the browser which actions are available
Media Session handlers register what the user can do. Only set handlers for actions your app actually supports:
function setAvailableActions(canSkip, canSeek) {
navigator.mediaSession.setActionHandler('play', () => audio.play());
navigator.mediaSession.setActionHandler('pause', () => audio.pause());
if (canSkip) {
navigator.mediaSession.setActionHandler('previoustrack', () => playPrevious());
navigator.mediaSession.setActionHandler('nexttrack', () => playNext());
} else {
navigator.mediaSession.setActionHandler('previoustrack', null);
navigator.mediaSession.setActionHandler('nexttrack', null);
}
if (canSeek) {
navigator.mediaSession.setActionHandler('seekto', (details) => {
audio.currentTime = details.seekTime;
});
} else {
navigator.mediaSession.setActionHandler('seekto', null);
}
}Call setAvailableActions() whenever track information or app state changes—for example, when a playlist ends or when the current track doesn't have siblings.
Cable foreground and background states
When the PWA is in the background, the service worker or page might not receive all events immediately. Handle focus/visibility changes to keep media state consistent:
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
// Update media session to match actual current time
updateMediaSession();
}
});
// In service worker, listen for media session events dispatched to clients
self.addEventListener('message', (event) => {
if (event.data.type === 'MEDIA_ACTION') {
switch (event.data.action) {
case 'play':
// Schedule playback in client
event.ports[0].postMessage({ action: 'play' });
break;
case 'pause':
event.ports[0].postMessage({ action: 'pause' });
break;
}
}
});Some browsers dispatch Media Session actions directly to the active worker, others dispatch to the page. Test on mobile devices where lock screen controls matter most.
Checklist: Media Session readiness for installable PWAs
- [ ] Media Session metadata is set after
play()fires, not earlier. - [ ] Playback position updates continuously with
timeupdateandseekedevents. - [ ] Action handlers are updated dynamically based on available actions (skip/seek/stop).
- [ ] Media state is re-synced when the page becomes visible again.
- [ ] Service worker or message channel handles background action dispatching on your target browsers.
- [ ] Artwork URLs work without authentication and use HTTPS.
- [ ]
setPositionStateis used only when supported, with a fallback for older browsers. - [ ] Actions are nullified when unavailable to prevent confusing OS controls.
What this means for media PWAs as installable apps
Installability brings the expectation that playback continues reliably acrossApp switches. Media Session is part of that expectation: lock screen controls, headset buttons, and notification playback controls should all work consistently. When users install a media PWA, they assume native-like playback integration—this API is how you deliver that experience.
Treat Media Session as part of your playback core, not an optional enhancement. The integration works best when playback state is the source of truth and the OS controls are merely a UI layer reflecting that state.