Modify Media Notifications with Media Sessions API

Christian Nwamba

The Media Sessions API provides a way to customize media notifications. Let’s imagine this scenario. You have multiple tabs opened with audio and videos playing on each of them and let’s say you want to pause the tab playing “N.Y State of Mind by Nas”, and you don’t want to go to that specific tab to pause the audio. You wished you had a way to control media playback right? That’s where Media Sessions API come in.

The Media Sessions API customizes media notifications by providing metadata for display by the user agent for the media your web app is playing. The API provides us with two interfaces namely;

  • MediaSession
  • MediaMetadata

The MediaSession interface is what allows the user to control media playback actions such as play, pause, previoustrack, nexttrack, etc. The MediaMetadata interface as it name implies, allows us to add metadata of the media we want to play. It creates a MediaMetadata object that consists of the following properties;

  • MediaMetadata.title
  • MediaMetadata.artist
  • MediaMetadata.album
  • MediaMetadata.artwork

In this article, we’ll be building a media controller for audio tracks using the Media Sessions API and Cloudinary for the audio files.

Pre-requisites

To flow with this tutorial, you’ll need the following:

  • Knowledge of JavaScript
  • A code editor (VS Code preferably)

The complete code and demo is here on Codesandbox.

Building the Media Controller

Firstly, if you want to follow through with the article, you should create a cloudinary account, and upload some mp3 files and images in seperate folders. This is how it looks on my end:

  • I have a music folder

  • Inside the music folder, i have two more folders for individual artists

  • Each of the folders contain an audio file and images of different sizes

Now we’ve set up the folder structure on cloudinary, let’s go ahead and build the media controller. Create a folder, i’ll call mine media-notifications-controller, open it with your code editor and create an index.html. Add these lines of code to the HTML file:

1//index.html
2<!DOCTYPE html>
3<html lang="en">
4 <head>
5 <meta charset="UTF-8">
6 <meta http-equiv="X-UA-Compatible" content="IE=edge">
7 <meta name="viewport" content="width=device-width, initial-scale=1.0">
8 <title>MediaSession API for Audio</title>
9 </head>
10 <body>
11 <div>
12 <h1> Media Session API for Audio </h1>
13 <button id="playButton">Start Playlist</button>
14 </div>
15 <script src="/main.js"></script>
16 </body>
17</html>

Here, we have a button with an id playButton, we’ll be using that id soon in the main.js JavaScript file. So let’s go ahead and create a main.js. Add these lines of code:

1//main.js
2let audio = document.createElement('audio');
3const button = document.getElementById('playButton')
4button.addEventListener('click', onPlayButtonClick)
5let playlist = getAwesomePlaylist();
6let index = 0;
7function onPlayButtonClick() {
8 playAudio();
9}

Firstly, we are creating an audio element (we’ll get to it very soon). Then we say, whenever a user clicks the START PLAYLIST button, it should invoke the onPlayButtonClick() function. The onPlayButtonClick() calls another function called playAudio(). Let’s now create the playAudio() function. Add these lines of code:

1//main.js
2function playAudio() {
3 audio.src = playlist[index].src;
4 audio.play()
5 .then(_ => updateMetadata())
6 .catch(error => console.log(error));
7}

The first thing that happens here is assigning playlist audio source (we’ll be creating soon) to the HTML audio element we created earlier. Next, we invoked the audio.play() method and then we return a Promise to invoke the updateMetadata() function if successful, if not, we print the error to our browser console. Let’s now create the updateMetadata(). Add these lines of code:

1//main.js
2
3function updateMetadata() {
4 let track = playlist[index];
5 console.log('Playing ' + track.title + ' track...');
6 navigator.mediaSession.metadata = new MediaMetadata({
7 title: track.title,
8 artist: track.artist,
9 album: track.album,
10 artwork: track.artwork
11 });
12 updatePositionState();
13}

Here, we introduce the MediaMetadata interface of the Media Sessions API. This will enable us add information such as title, artist, album and artwork to the Media Controller. We invoke a updatePositionState() function, this enables us to set playback position. It’s important to know that the position state is a combination of the media playback rate, duration, and current time. Let’s go ahead and create that function. Add these lines of code:

1//main.js
2function updatePositionState() {
3 if ('setPositionState' in navigator.mediaSession) {
4 console.log('Updating position state...');
5 navigator.mediaSession.setPositionState({
6 duration: audio.duration,
7 playbackRate: audio.playbackRate,
8 position: audio.currentTime
9 });
10 }
11}

You will notice that setPositionState accepts duration, playbackRate and position as arguments and we are getting the values from our audio element. Next, we’ll handle actions. To handle actions we will use the MediaSession interface method setActionHandler(). Let’s create the action handler for playing previous track. Add these lines of code:

1//main.js
2navigator.mediaSession.setActionHandler('previoustrack', function () {
3 console.log('You clicked "Previous Track" icon.');
4 index = (index - 1 + playlist.length) % playlist.length;
5 playAudio();
6});

The action handler for next track. Add these lines of code:

1//main.js
2navigator.mediaSession.setActionHandler('nexttrack', function () {
3 console.log('> You clicked "Next Track" icon.');
4 index = (index + 1) % playlist.length;
5 playAudio();
6});

The action handler for when a track has ended.

1audio.addEventListener('ended', function () {
2 index = (index - 1 + playlist.length) % playlist.length;
3 playAudio();
4});

When a track ends, this action handler enables the controller to play the next track automatically.

Typically, a media controller will have fast forward and backwards buttons. Let’s create the action handlers for each of them. The action handler to go backwards. Add these lines of code:

1//main.js
2let defaultSkipTime = 10; /* Time to skip in seconds by default */
3 navigator.mediaSession.setActionHandler('seekbackward', function (event) {
4 console.log('You clicked "Seek Backward" icon.');
5 const skipTime = event.seekOffset || defaultSkipTime;
6 audio.currentTime = Math.max(audio.currentTime - skipTime, 0);
7 updatePositionState();
8});

The action handler to fast forward. Add these lines of code:

1//main.js
2navigator.mediaSession.setActionHandler('seekforward', function (event) {
3 console.log('You clicked "Seek Forward" icon.');
4 const skipTime = event.seekOffset || defaultSkipTime;
5 audio.currentTime = Math.min(audio.currentTime + skipTime, audio.duration);
6 updatePositionState();
7});

Next, we’ll create action handlers for play and pause buttons. Add these lines of code:

1//main.js
2navigator.mediaSession.setActionHandler('play', async function () {
3 console.log('You clicked "Play" icon.');
4 await audio.play();
5});
1//main.js
2navigator.mediaSession.setActionHandler('pause', function () {
3 console.log('You clicked "Pause" icon.');
4 audio.pause();
5});

Sometimes when fast forwarding or loading a track, the browser may consider the webpage to not be playing the audio track, we have to override this behavior. Add these lines of code to do that:

1//main.js
2audio.addEventListener('play', function () {
3 navigator.mediaSession.playbackState = 'playing';
4});
5audio.addEventListener('pause', function () {
6 navigator.mediaSession.playbackState = 'paused';
7});

Let’s create one last action handler. This will be for the stop action. Add these lines of code:

1//main.js
2
3try {
4 navigator.mediaSession.setActionHandler('stop', function () {
5 console.log('> User clicked "Stop" icon.');
6 });
7} catch (error) {
8 console.log('Warning! The "stop" media session action is not supported.');
9}

Awesome! We are almost done. Let’s now go ahead and create the getAwesomePlaylist function. This function will return the track data. Add these lines of code:

1//main.js
2
3function getAwesomePlaylist() {
4 const BASE_URL = 'https://res.cloudinary.com/sammy365/video/upload/v1658489677/music/';
5 const IMG_BASE_URL = 'https://res.cloudinary.com/sammy365/image/upload/v1658489645/music/';
6
7return [{
8 src: BASE_URL + 'Asake/Asake-PBUY.mp3',
9 title: 'PBUY',
10 artist: 'Asake',
11 album: 'Asake Reloaded',
12 artwork: [
13 { src: IMG_BASE_URL + 'Asake/asake-96.png', sizes: '96x96', type: 'image/png' },
14 { src: IMG_BASE_URL + 'Asake/asake-128.png', sizes: '128x128', type: 'image/png' },
15 { src: IMG_BASE_URL + 'Asake/asake-192.png', sizes: '192x192', type: 'image/png' },
16 { src: IMG_BASE_URL + 'Asake/asake-384.png', sizes: '384x384', type: 'image/png' },
17 { src: IMG_BASE_URL + 'Asake/asake-512.png', sizes: '512x512', type: 'image/png' },
18 ]
19 },
20 {
21 src: BASE_URL + 'olamide/Bad_Boy_Timz_-_Skelele_Official_Audio_ft._Olamide.mp3',
22 title: 'Skelele',
23 artist: 'Olamide',
24 album: 'Olamide Reloaded',
25 artwork: [
26 { src: IMG_BASE_URL + 'olamide/olamide-96.png', sizes: '96x96', type: 'image/png' },
27 { src: IMG_BASE_URL + 'olamide/olamide-128.png', sizes: '128x128', type: 'image/png' },
28 { src: IMG_BASE_URL + 'olamide/olamide-192.png', sizes: '192x192', type: 'image/png' },
29 { src: IMG_BASE_URL + 'olamide/olamide-384.png', sizes: '384x384', type: 'image/png' },
30 { src: IMG_BASE_URL + 'olamide/olamide-512.png', sizes: '512x512', type: 'image/png' },
31 ]
32 }
33];
34}

Yep! that’s it. Open the website with Live Server and you should see something like this:

https://www.dropbox.com/s/202qnwf49e34dc4/mediaSession.webm?dl=0

Conclusion

In this article, we explored the MediaSessions API. We discussed about it’s two interfaces namely; MediaSession and MediaMetadata. We went ahead to build a media controller with JavaScript using the MediaSessions API. I hope you’ve picked up a thing or two from this article.

Further Reading

Happy Coding!

Christian Nwamba

Developer Advocate at AWS

A software engineer and developer advocate. I love to research and talk about web technologies and how to delight customers with them.