Building a Mobile JavaScript Powered Audio Player

Aurelio De Rosa
Share

As some of you might know, I’m addicted to HTML5 and JavaScript APIs.

I’ve written many tutorials discussing APIs such as the getUserMedia API, the Web Speech API, the Screen Orientation API, and I also have a dedicated repository on GitHub.

In this article I’ll show you how to create a simple JavaScript API-powered audio player that employs several APIs to improve the experience for people using a mobile device.

A JavaScript API Powered Audio Player

In this tutorial I’ll use the following APIs:

  • The Ambient Light API to change the theme of the web page based on the light level of the surrounding environment.
  • The Proximity API to play/pause audio based on the proximity of an object.
  • The Battery Status API to detect the battery level and automatically pause audio when the battery is running critically low.
  • The Web Notifications API to notify the user when the battery is running low and that the audio was paused because of this.
  • The Vibration API to provide tactile feedback that reinforces the notification message described above.

If you need a refresher of one or more of these APIs, take a look at the articles linked because this tutorial will assume that you know how to work with them.

This demo will use the native HTML5 audio element to play audio without any library as a fallback. However, a message will be shown in case the browser doesn’t support the audio element.

The whole application will be developed with progressive enhancement in mind. If a browser doesn’t support one or more of the previously cited APIs, the application will continue to work properly. The only difference is that the browser won’t take advantage of the feature that employs the unsupported API.

Creating the markup

The demo is made of a single HTML page with very simple markup. It’s made of a short summary of the experiment and the audio element with the native controls enabled (controls attribute). The page has a link and a script element. The former refers to the CSS file (discussed in the next section) containing the declaration blocks to define the three different themes (more on this in the next section). The latter points to the JavaScript file containing the business logic of the experiment.

As you can see in the code below, the body element has a predefined class attribute’s value of normal-theme. It represents the default theme that is used under normal light conditions.

The full code of the HTML page is listed below:

<!DOCTYPE html>
<html lang="en">
   <head>
      <meta charset="utf-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <title>Mobile Audio Player</title>
      <meta name="description" content="APIs-powered Audio Player">
      <meta name="viewport" content="width=device-width, initial-scale=1">

      <link rel="stylesheet" href="css/main.css">
   </head>
   <body class="normal-theme">
      <h1>APIs-powered Audio Player</h1>
      <p>
         This demo shows how to create a simple APIs-powered audio player. In particular this page
         uses the Proximity API, the Battery Status API, the Vibration API, the Web Notifications API,
         and the Ambient Light API.
      </p>
      <audio id="audio" src="http://freshly-ground.com/data/audio/mpc/20090119%20-%20Untitled%20Groove.mp3" controls>
         <p>I'm sorry but your browser doesn't support the <code>audio</code> element, so you can't run the demo.</p>
      </audio>

      <script src="js/main.js" async></script>
   </body>
</html>

The Player themes

The CSS file of the experiment is very simple and short. It defines several rules for the body element and three themes: dark-theme, normal-theme, and light-theme. Each of these themes define a color for the background and one for the text of the page.

In a dark environment, to avoid causing stress to the eyes of the users we’ll set a dark background with a light color for the text. Conversely, in a bright environment, we’ll adopt a light background with a dark color for the text. Under normal light conditions (the default) we use the combination that we like the most, provided that our users are still able to use the page (for example, red text on a red background isn’t a good choice).

The full CSS code is below:

body
{
   max-width: 600px;
   margin: 0 auto;
   font-size: 20px;
   padding: 0 1em;
}

.dark-theme
{
   background-color: #000000;
   color: #FFFFFF;
}

.normal-theme
{
   background-color: #B8FFF7;
   color: #C53131;
}

.light-theme
{
   background-color: #FFFFFF;
   color: #000000;
}

The business logic

The business logic is the most exciting part of this experiment. Here we’ll cover the code that powers the audio player and how to build the features described at the beginning of the article.

The first step we need to perform is to test the support for the APIs we plan to use and store the results as properties of a literal object:

var tests = {
   proximity: 'onuserproximity' in window,
   light: 'ondevicelight' in window,
   vibration: 'vibrate' in window.navigator,
   notification: 'Notification' in window
};

As you can see the test for the Battery Status API is missing. Firefox implements an old version of the specifications that isn’t Promise-based, thus we’ll treat this API as a case on its own. In this demo I wanted to support both versions because Firefox is the only browser that implements all the APIs employed in this experiment. I thought it was important to have at least one browser able to expose all the features of the demo.

In addition to the test variable, we also need a config variable defined as follows:

var config = {
   battery: {
      lowThreshold: 0.15,
      criticalThreshold: 0.05
   },
   vibration: {
      lowThreshold: [500, 200, 500],
      criticalThreshold: [1000]
   },
   notification: {
      lowThreshold: {
         tTitle: 'Battery level: low',
         message: 'Please charge your device to avoid the audio to be automatically paused.'
      },
      criticalThreshold: {
         title: 'Battery level: critical',
         message: 'The audio has been stopped to avoid the shutdown of your device.'
      }
   },
   light: {
      darkThreshold: 50,
      normalThreshold: 10000
   }
};

It contains data that we’ll use in combination with the JavaScript APIs. For example, we have defined the thresholds to use with the Battery Status API (under the battery property) to specify when our application will consider the battery level to be low or critical. We have also defined the vibration patterns (under the vibration property) to use when the battery level is low (lowThreshold) or critical (criticalThreshold). Finally, we have defined properties to employ with the Web Notifications API (notification property) and the Ambient Light API (light property), to specify when we’ll consider the light level to be low and normal.

The last step of preparation we need to perform is to retrieve the audio element. This is achieved with the following statement:

var audio = document.getElementById('audio');

At this point we’re ready to employ the JavaScript APIs to give superpowers to our audio player. The first feature we’ll implement is the gesture to play/pause the audio. To be precise, we’ll not implement a real gesture. Placing a finger, the hand, or any other object close enough to the proximity sensor will be enough to play/pause the audio, but calling it a “gesture” sounds better.

This feature is implemented with the following code:

if (tests.proximity) {
   window.addEventListener('userproximity', function (event) {
      if (event.near) {
         audio.paused ? audio.play() : audio.pause();
      }
   });
}

Easy, isn’t it? Another easy feature we can create is to switch the theme applied based on the environment light level. Listening for a change of the light level, we can detect if it’s below the darkness threshold defined (darkThreshold), between the latter and the normal threshold (normalThreshold), or above the normal threshold. Once done, we can change the theme accordingly. Turning this description into code results in the following snippet:

if (tests.light) {
   window.addEventListener('devicelight', function(event) {
      var light = Math.round(event.value);

      if (light < config.light.darkThreshold) {
         document.body.className = 'dark-theme';
      } else if (light < config.light.normalThreshold) {
         document.body.className = 'normal-theme';
      } else {
         document.body.className = 'light-theme';
      }
   });
}

Now that we’ve got the code to detect the change of the light level and to detect the “gesture” to play/pause the audio, we have to implement the features related to the battery level. To do that we have to attach a handler to the levelchange event and run the same handler as soon as the application starts. Doing so, if the battery level is in a low or critical state when the application starts we’ll be able to act accordingly. For this purpose, we’ll define a manageBattery() function. We’ll also detect the version of the Battery Status API supported by the browser to know if we can attach the handler directly or when the Promise is resolved.

The resulting code is listed below:

function manageBattery(battery) {
   // Code here...
}

if (window.navigator.getBattery) {
   window.navigator.getBattery().then(function(battery){
      battery.addEventListener('levelchange', manageBattery.bind(window, battery));
      manageBattery(battery);
   });
} else if (window.navigator.battery) {
   window.navigator.battery.addEventListener('levelchange', manageBattery.bind(window, window.navigator.battery));
   manageBattery(window.navigator.battery);
}

The last step to perform is to create the body of the manageBattery() function. Inside this function we have to perform the following operations:

  1. Detect the battery level (good, low, or critical)
  2. Pause the audio if the battery level is critical
  3. Vibrate the device using a different pattern depending on the battery level (low or critical)
  4. Show a different notification message about the state of the battery depending on its level (low or critical)

Based on this list, the resulting code is as follows:

function manageBattery(battery) {
   if(!battery.charging && audio.duration > 0 && !audio.paused) {
      if (battery.level > config.battery.lowThreshold) {
         return;
      }

      var isCritical = battery.level <= config.battery.criticalThreshold;
      if (isCritical) {
         audio.pause();
      }

      if (tests.vibration) {
         window.navigator.vibrate(
            isCritical ? config.vibration.criticalThreshold : config.vibration.lowThreshold
         );
      }

      if (tests.notification) {
         Notification.requestPermission(function(permission) {
            if (permission !== 'denied') {
               new Notification(
                  isCritical ?  config.notification.criticalThreshold.title : config.notification.lowThreshold.title,
                  {
                     body: isCritical ?
                        config.notification.criticalThreshold.message :
                        config.notification.lowThreshold.message
                  }
               );
            }
         });
      }
   }
}

With this last snippet we’ve finished our demo and it is now ready to be tested.

Code and live demo

The complete and working code for this experiment can be found on GitHub. If you want to see the code we’ve developed in action, a live demo is also available.

Conclusion

In this tutorial we’ve developed a simple yet functional audio player that used several new JavaScript APIs. With this experiment I’ve proved that using the JavaScript APIs you can create powerful applications that have mobile-focused features, and improve the experience of your users. I hope you liked it and you found this demo fun.