The Particle Lab

Illustration for Building a Custom HTML5 Audio Player with jQuery

Building a Custom HTML5 Audio Player with jQuery

By Ben, 02 Sep 2010

We recently built an HTML5 audio player for Tim Van Damme's The Box, a new podcast where he interviews people who make cool stuff. Tim wanted an HTML5 audio player on the site, and we put together some jQuery to hook up the player interface he designed. In this article we'll run through the code to explain how it works, covering a few caveats along the way.

Here's the player interface, and the markup for it.

<p class="player">
  <span id="playtoggle" />
  <span id="gutter">
    <span id="loading" />
    <span id="handle" class="ui-slider-handle" />
  </span>
  <span id="timeleft" />
</p>

As you can see, we have a few span elements for each component of the interface:

We won't cover the CSS for the player here, but if you want to see how it's styled you can inspect the styles on the live site.

We're going to use jQuery to detect support for HTML5 audio, and if it's supported, we'll insert the audio player markup and the audio tag itself. This means that browsers that don't have HTML5 audio support won't see an audio player interface they can't use. You could optionally fall back to a Flash based player, but as Tim is already providing a direct link to the MP3 file elsewhere on the page, the audio player is seen as progressive experience enrichment here.

if(!!document.createElement('audio').canPlayType) {

  var player = '<p class="player"> ... </p>\
    <audio>\
      <source src="/path/to/episode1.ogg" type="audio/ogg"></source>\
      <source src="/path/to/episode1.mp3" type="audio/mpeg"></source>\
      <source src="/path/to/episode1.wav" type="audio/x-wav"></source>\
    </audio>';

  $(player).insertAfter("#listen .photo");

}

The detection code on line 1 is taken from a guide to feature detection on the superb Dive Into HTML5 site. If support is detected, we go right ahead and insert the player code (abridged at the "..." in this code), and the audio tag.

Your audio tag should contain three source formats. The first is OGG Audio which is an open standard, supported by Firefox and Chrome. The second is our old friend MPEG-1 layer 3 (MP3) which is a commercial proprietary format supported by Safari. The third is a plain old WAVE file, which is what Opera wants to hear. Browsers will attempt to use sources in markup order, so for example Safari would fail to read the OGG format source, and use the MP3 source instead, while Opera will fail on the first two sources and use the third WAVE source.

Now we need to start adding functionality to our player:

audio = $('.player audio').get(0);
loadingIndicator = $('.player #loading');
positionIndicator = $('.player #handle');
timeleft = $('.player #timeleft');

if ((audio.buffered != undefined) && (audio.buffered.length != 0)) {
  $(audio).bind('progress', function() {
    var loaded = parseInt(((audio.buffered.end(0) / audio.duration) * 100), 10);
    loadingIndicator.css({width: loaded + '%'});
  });
}
else {
  loadingIndicator.remove();
}

We first save variable references to several key elements of our player so that we can refer to them quickly without querying the DOM each time.

We then look for support of the buffered property on our audio tag. This should contain information about how much of the audio file has been buffered. At the time of writing, Firefox doesn't provide the buffered property at all, while Opera has the property but doesn't put anything in it. For browsers that do (and for future versions of Firefox and Opera that have full support for the buffered property), we set up an event handler for the 'progress' event (fired as loading progress is made). As the audio file loads, we calculate the amount of the file that has been loaded as a percentage, and then use this value for the width of our loading indicator. If buffered progress support isn't available, we can simply remove the redundant loading indicator element from the DOM.

Now we'll write an event handler for the 'timeupdate' event, which is fired whenever the current play time is updated, either as we're playing the audio normally, or when we seek to a new position within the audio file.

$(audio).bind('timeupdate', function() {
		
  var rem = parseInt(audio.duration - audio.currentTime, 10),
  pos = (audio.currentTime / audio.duration) * 100,
  mins = Math.floor(rem/60,10),
  secs = rem - mins*60;
			
  timeleft.text('-' + mins + ':' + (secs > 9 ? secs : '0' + secs));
  if (!manualSeek) { positionIndicator.css({left: pos + '%'}); }
  if (!loaded) {
    loaded = true;
				
    $('.player #gutter').slider({
      value: 0,
      step: 0.01,
      orientation: "horizontal",
      range: "min",
      max: audio.duration,
      animate: true,					
      slide: function() {							
        manualSeek = true;
      },
      stop:function(e,ui) {
        manualSeek = false;					
        audio.currentTime = ui.value;
      }
    });
  }

});

First we calculate the play time remaining (in seconds), the position of the playhead as a percentage of total duration, and the minutes and seconds remaining.

We then update the displayed time remaining in the player interface, taking care to insert a leading 0 if necessary to the seconds figure.

If we haven't triggered this event by manually seeking (determined a bit further on in the code), we move the playhead position indicator along the gutter track. This basically just slides the indicator along as we listen, unless we're using the slider mechanism to move the playing position, in which case we don't want to interfere with the position of the indicator because the slider code will handle that for us.

Until the audio has started to load, the duration of the audio file is not available. We therefore check to see if the audio file has started to load, and proceed to set up the slider for the draggable playhead control. We're using jQuery UI for our slider here, and the basic configuration options should be self explanatory. We add two event handlers; one on slide where we set a flag that we are manually seeking to a new position, and stop, to unset the flag and tell the browser's audio player that it needs to move to our new position in the audio file.

The only thing left is the play/pause toggle button.

$(audio).bind('play',function() {
  $("#playtoggle").addClass('playing');		
}).bind('pause ended', function() {
  $("#playtoggle").removeClass('playing');		
});		
		
$("#playtoggle").click(function() {			
  if (audio.paused) { audio.play(); } 
  else { audio.pause(); }			
});

First we set up a couple of event handlers. When we start playing the audio, we add a class of 'playing' to the button so that it switches to the pause state. We then remove that class if we pause the audio playback, or reach the end of the file which fires the 'ended' event.

Our click handler for the play/pause button is very simple; play the audio if we're currently paused, otherwise pause it.

So that's it for a super simple audio player using lovely web standards instead of Flash. By going down this route, Tim's visitors can use his audio player on mobile devices like the iPhone and iPad, and the loading performance of the player is hopefully better than would have been achievable with a plugin based solution. Go and listen to The Box, and if you have any questions or ideas for improving the player, please leave a comment.

UPDATE: Fixed the code for Opera, providing the WAVE source and a more detailed check for support of the buffered property.

UPDATE 2: Fixed a typo binding the play event handler, which should be $(audio).bind not audio.bind. Thanks to @patdryburgh for helping find that.

Useful Resources:

Photo credit: Miikka Skaffari
Photo of Ben Bodien, who wrote this blog post

Ben Bodien is Principal & Co-Founder at Neutron Creations, where he oversees product design and front-end development. For a somewhat balanced mixture of ranting and raving follow Ben Bodien on twitter.

comments powered by Disqus