“Even a mistake may turn out to be the one thing necessary to a worthwhile achievement.” --Henry Ford

How To: Create a Custom HTML5 Audio Player

$count: 355

The HTML5 audio tag is great. You can add it directly to any web page to play an audio file without any extra programming such as javascript or any third-party application like Adobe Flash Player. However, each browsers controls are different. The audio controls in Microsoft Internet Explorer look very different from that of Firefox or Google Chrome. And, these native controls do not have any built-in playlist capabilities. For example, there are no controls to navigate to the next song or track nor go back to a previous song or track. To have these features and keep the same look in any HTML5 browser we will have to create our own. There are a lot of ways you can accomplish this, Here is one solution using jQuery and the jQuery User Interface (jQuery UI).

How to create it

Step one:
Start by adding a <div> tag to your HTML. Be sure to include a unique id. I used "ap_container_1".
Also make sure to include both the jQuery library and the jQuery UI library (see Related articles below).

<div id="ap_container_1"></div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.min.js"></script>
</body>
</html>
Step one

Now start a new javascript file and declare some variables that will be used within the jQuery script.

  1. vavar iTrack = 0,
  2.     isPlaying = false,
  3.     cpa = 0,
  4.     duration = 0,
  5.     sp = 0,
  6.     tracks = [],
  7.     trackCount = 0,
  8.     aS,
  9.     $title,
  10.     $album,
  11.     $artist,
  12.     $play,
  13.     $paus,
  14.     $dura,
  15.     $prog,
  16.     $time,
  17.     $mute,
  18.     $unmu,

Why do some variables have a $ sign in front and some do not? The variables with the $ sign signify that that variable holds a jQuery object and the other variables hold mixed values but no jQuery object.

Next, create a function to determine if the browser supports the HTML5 <audio> tag and which formats.

  1.     audioSupport = function() {
  2.         var elem = document.createElement('audio'),
  3.             bool = false;
  4.         try {
  5.             if(bool = !!elem.canPlayType) {
  6.                 bool = new Boolean(bool);
  7.                 bool.ogg = elem.canPlayType('audio/ogg; codecs="vorbis"');
  8.                 bool.mp3 = elem.canPlayType('audio/mpeg;');
  9.                 bool.wav = elem.canPlayType('audio/wav; codecs="1"');
  10.                 bool.m4a = (elem.canPlayType('audio/x-m4a;') || eleam.canPlayType('audio/aac;'));
  11.             }
  12.         } catch(e) { }
  13.         return bool;
  14.     };

What is happening in this function?

  • line 20: An audio element is created and assigned to the variable named elem (short for element).
  • line 21: Declare a variable named bool (short for Boolean) and assign it a value of false.
  • line 23: Check if the audio element can play any type of audio file and assign the result to bool.
  • line 24: Create a new Boolean object and assign it to bool setting the value from bool.
  • lines 25-28: Check each file format to determine if it is supported and extend the Boolean object.
  • line 31: Return the Boolean object assigned to the variable bool.

Now we call the audioSupport function we just created and assign the result to the variable named "aS" and check the value at the same time.

  1. if(aS = audioSupport()) {

If the browser supports the HTML5 <audio> tag and can play at least one of the four audio file formats then we start to build the custom audio player interface. The majority of the remaining code is all variable declarations and assignment. We will create functions in this manner as well (like the audioSupport function above).

  1. var manualSeek = false,
  2.     audio = $('<audio/>').on({
  3.         'play': function() {
  4.             $paus.show();
  5.             $play.hide();
  6.         },
  7.         'pause': function() {
  8.             isPlaying = false;
  9.             showPlay();
  10.         },
  11.         'playing': function() {
  12.             isPlaying = true;
  13.         },
  14.         'ended': function() {
  15.             showPlay();
  16.             trackCount ? playNext('ended') : resetMedia();
  17.         },
  18.         'loadedmetadata': function() {
  19.             if(isFinite(audio.duration)) {
  20.                 duration = audio.duration;
  21.             }
  22.             updateTimes(audio.duration, audio.currentTime);
  23.             if($prog.slider('option', 'disabled')) {
  24.                 $prog.slider('option', 'disabled', false);
  25.             }
  26.         },
  27.         'timeupdate': function() {
  28.             updateTimes(audio.duration, audio.currentTime);
  29.             if(!manualSeek) {
  30.                 $prog.slider('value', cpa);
  31.             }
  32.         }
  33.     }).get(0),
  34.     ext = aS.mp3 ? '.mp3' : aS.ogg ? '.ogg' : '',

What this code is doing:

  • line 35: Create an audio element and start event handlers. The jQuery function "on" is the preferred method of assigning object event handlers. These event handlers are triggered when the object receives or generates the specified event. In this case it is the audio object Media events:
    • 'play' - Script to be run when the media is ready to start playing
    • 'pause' - Script to be run when the media is paused either by the user or programmatically
    • 'playing' - Script to be run when the media actually has started playing
    • 'ended' - Script to be run when the media has reach the end
    • 'loadedmetadata' - Script to be run when meta data (like dimensions and duration) are loaded
    • 'timeupdate' - Script to be run when the playing position has changed
  • line 66: Get the DOM node instead of the jQuery object.
  • line 67: Assign a value to the variable named "ext" (short for extension). The variable named "aS" contains the extended Boolean object created in and returned by the audioSupport function as assigned on line 34 above. There are four possible values for each of the file formats in the extended Boolean object; blank or undefined, either of which will evaluate false, or "maybe" or "possibly" which will evaluate true.

Next we will start to define the visible components of the interface. I will warn you right now that this section of code can get confusing and a little tricky because we will be nesting items together and you have to pay close attention to make sure things are where you expect them to be.

  1.     mp = $('#ap_container_1').append($('<div/>', {
  2.         'class': 'ap-info-panel ui-widget ui-widget-content ui-corner-all'
  3.     }).append($title = $('<h3/>', {
  4.         'class': 'ap-title'
  5.     })).append($album = $('<h4/>', {
  6.         'class': 'ap-album'
  7.     })).append($artist = $('<h5/>', {
  8.         'class': 'ap-artist'
  9.     }))).append($('<div/>', {
  10.         'class': 'ap-player ui-widget ui-widget-content ui-corner-all'
  11.     }).append($('<ul/>', {
  12.         css: {
  13.             'list-style': 'none',
  14.             margin: 0,
  15.             padding: 0
  16.         },
  17.         'class': 'ap-controls-content',
  18.         click: function(e) {
  19.             var t = e.target, $t = $(t), btn;
  20.             if(t.nodeName.toLowerCase() === 'a') {
  21.                 e.preventDefault();
  22.                 btn = $t.parent().data('btn');
  23.                 $t.blur();
  24.                 if(btn === 'prev') {playPrev();}
  25.                 if(btn === 'play') {audio.play();}
  26.                 if(btn === 'paus') {audio.pause();}
  27.                 if(btn === 'next') {playNext('click');}
  28.                 if(btn === 'mute') {
  29.                     audio.muted = true;
  30.                     $unmu.show();
  31.                     $mute.hide();
  32.                 }
  33.                 if(btn === 'unmu') {
  34.                     audio.muted = false;
  35.                     $mute.show();
  36.                     $unmu.hide();
  37.                 }
  38.             }
  39.         },
  40.         dblclick: function(e) {
  41.             var t = e.target, $t = $(t), btn;
  42.             if(t.nodeName.toLowerCase() === 'a') {
  43.                 e.preventDefault();
  44.                 btn = $t.parent().data('btn');
  45.                 $t.blur();
  46.                 if(btn === 'prev') {_playPrev();}
  47.             }
  48.         }
  49.     }).append($('<li/>', {
  50.         'class': 'ap-controls ap-prev ui-state-default ui-corner-all'
  51.     }).data('btn', 'prev').append($('<a/>', {
  52.         'class': 'ap-button ui-icon ui-icon-seek-first',
  53.         title: 'prev',
  54.         href: '#',
  55.         text: 'Prev'
  56.     }))).append($play = $('<li/>', {
  57.         'class': 'ap-controls ap-play ui-state-default ui-corner-all'
  58.     }).data('btn', 'play').append($('<a/>', {
  59.         'class': 'ap-button ui-icon ui-icon-play',
  60.         title: 'play',
  61.         href: '#',
  62.         text: 'Play'
  63.     }))).append($paus = $('<li/>', {
  64.         css: {
  65.             display: 'none'
  66.         },
  67.         'class': 'ap-controls ap-pause ui-state-default ui-corner-all'
  68.     }).data('btn', 'paus').append($('<a/>', {
  69.         'class': 'ap-button ui-icon ui-icon-pause',
  70.         title: 'pause',
  71.         href: '#',
  72.         text: 'Pause'
  73.     }))).append($('<li/>', {
  74.         'class': 'ap-controls ap-next ui-state-default ui-corner-all'
  75.     }).data('btn', 'next').append($('<a/>', {
  76.         'class': 'ap-button ui-icon ui-icon-seek-end',
  77.         title: 'next',
  78.         href: '#',
  79.         text: 'Next'
  80.     }))).append($mute = $('<li/>', {
  81.         'class': 'ap-controls ap-mute ui-state-default ui-corner-all'
  82.     }).data('btn', 'mute').append($('<a/>', {
  83.         'class': 'ap-button ui-icon ui-icon-volume-on',
  84.         title: 'mute',
  85.         href: '#',
  86.         text: 'mute'
  87.     }))).append($unmu = $('<li/>', {
  88.         css: {
  89.             display: 'none'
  90.         },
  91.         'class': 'ap-controls ap-unmute ui-state-default ui-corner-all'
  92.     }).data('btn', 'unmu').append($('<a/>', {
  93.         'class': 'ap-button ui-icon ui-icon-volume-off',
  94.         title: 'unmute',
  95.         href: '#',
  96.         text: 'unmute'
  97.     })))).append($('<div/>', {
  98.         'class': 'ap-progress'
  99.     }).append($('<div/>', {
  100.         css: {
  101.             position: 'relative',
  102.             'padding-bottom': '2px'
  103.         }
  104.     }).append($time = $('<div/>', {
  105.         'class': 'ap-time ap-current',
  106.         html: '0:00'
  107.     })).append($dura = $('<div/>', {
  108.         'class': 'ap-time ap-duration',
  109.         html: '0:00'
  110.     }))).append($('<div/>', {
  111.         css: {padding: '0 9px 0 7px'},
  112.         'class': 'ap-progress-content'
  113.     }).append($prog = $('<div/>', {
  114.         'class': 'ap-progress-slider'
  115.     }).slider({
  116.         disabled: true,
  117.         max: 100,
  118.         value: 0,
  119.         step: 0.01,
  120.         orientation: 'horizontal',
  121.         range: 'min',
  122.         animate: true,
  123.         slide: function(e, ui) {
  124.             manualSeek = true;
  125.             if(sp > 0) {
  126.                 var percent = ui.value * (100 / sp);
  127.                 if((typeof audio.seekable === 'object') && (audio.seekable.length > 0)) {
  128.                     audio.currentTime = percent * audio.seekable.end(audio.seekable.length-1) / 100;
  129.                 } else if(audio.duration > 0 && !isNaN(audio.duration)) {
  130.                     audio.currentTime = percent * audio.duration / 100;
  131.                 }
  132.             } else {
  133.                 resetMedia();
  134.             }
  135.         },
  136.         stop: function(e, ui) {
  137.             manualSeek = false;
  138.         }
  139.     }))))),

There are a lot of things happening in this code snippet. This is a great example of the power and flexibility of jQuery. We are able to create elements, set all the different settings and control features in one statement because jQuery allows chaining (the ability to chain more than one function at a time). Let me explain some of the code.

  • All of this code, lines 68 - 206, is appended to the HTML element id "ap_container_1" from step one.
  • lines 68-76: Create the information panel (not visible in the example above) which would show title, album, and artist information. This information would come from the source object described later.
    • lines 68-70: Create the info panel div element and add the appropriate classes.
    • line 70: Create an h3 element, and append it to the info panel div, to show title information. By assigning this jQuery object to a variable we can re-use it elsewhere in the code.
    • line 71: Add the appropriate class to the element
    • lines 72-76: Repeat of lines 70-72. Create elements for album and artist information, append them to the info panel div, and add the appropriate classes.

      Here is what lines 68 - 76 produce so you can see a more clear picture of how this works.

      <div class="ap-info-panel ui-widget ui-widget-content ui-corner-all">
        <h3 class="ap-title"></h3>
        <h4 class="ap-album"></h4>
        <h5 class="ap-artist"></h5>
      </div>
      Sample output - line breaks and spacing added for clarity
  • lines 76-206: Create the custom audio player controls and provide functionality.
  • lines 76-78: Create a div element as a container for the audio player controls and add appropriate classes.
  • lines 78-116: Create a unordered list element, sets in-line style, adds appropriate classes, and define event handlers for click and double click mouse events.
  • lines 116-164: Create list item elements, appended to the unordered list, and anchor elements, appended to each list item element, for each of the "button" controls, such as Play, Pause, Mute, etc.
  • lines 164-206: Create elements for the progress section of the custom audio player. Including a jQuery UI slider widget control.

We need some helper functions and an object to perform some repetitive tasks and format output.

  1.     showPlay = function() {
  2.         $play.show();
  3.         $paus.hide();
  4.     },
  5.     timeFormat = {
  6.         showHour: true,
  7.         showMin: true,
  8.         showSec: true,
  9.         padHour: false,
  10.         padMin: false,
  11.         padSec: true,
  12.         sepHour: ":",
  13.         sepMin: ":"
  14.     },
  15.     convertTime = function(s) {
  16.         s = isNaN(s) ? 0 : s;
  17.     var myTime = new Date(s * 1000),
  18.         hour = myTime.getUTCHours(),
  19.         min = myTime.getUTCMinutes(),
  20.         sec = myTime.getUTCSeconds(),
  21.         strHour = (timeFormat.padHour && hour < 10) ? "0" + hour : hour,
  22.         strMin = (hour > 0 || (timeFormat.padMin && min < 10)) ? "0" + min : min,
  23.         strSec = (timeFormat.padSec && sec < 10) ? "0" + sec : sec;
  24.         return ((timeFormat.showHour && hour > 0) ? strHour + timeFormat.sepHour : "") + ((timeFormat.showMin) ? strMin + timeFormat.sepMin : "") + ((timeFormat.showSec) ? strSec : "");
  25.     },
  26.     updateTimes = function(d, c) {
  27.         var r = parseInt(d, 10) - parseInt(c, 10);
  28.         cpa = (duration > 0) ? 100 * c / duration : 0;
  29.         if((typeof audio.seekable === 'object') && (audio.seekable.length > 0)) {
  30.             sp = (duration > 0) ? 100 * audio.seekable.end(audio.seekable.length-1) / duration : 100;
  31.         }
  32.         r = (r < 0) ? -r : r;
  33.         $dura.text(convertTime(r));
  34.         $time.text(convertTime(c));
  35.     },

Did you notice we are using some of the variables that hold jQuery objects?

  • lines 207-210: Creates a function, named showPlay, to show the play control and hide the pause control. The functions .show() and .hide() are jQuery library functions.
  • lines 211-220: Creates an object named timeFormat. This object will help format the times displayed.
  • lines 221-231: Creates a function, named convertTime, to convert seconds into a formatted time string.
  • lines 232-241: Creates a function, named updateTimes, to update the duration "d" and the current time "c".

The next section of code defines action functions for media selection and playback.

  1.     mediaStart = function() {
  2.         audio.currentTime = 0;
  3.     },
  4.     resetMedia = function() {
  5.         audio.pause();
  6.         setTimeout(function() {
  7.             $prog.slider('value', 0);
  8.         }, 0);
  9.         mediaStart();
  10.     },
  11.     playPrev = function() {
  12.         isPlaying ? mediaStart() : _playPrev();
  13.     },
  14.     playNext = function( s ) {
  15.         (s === 'ended') ? _playNextEnded() : _playNext();
  16.     },
  17.     _playPrev = function() {
  18.         if( trackCount ) {
  19.             --iTrack;
  20.             if( trackCount && iTrack ) {
  21.                 if( isPlaying ) {
  22.                     _playTrack( iTrack );
  23.                 } else {
  24.                     _loadTrack( iTrack );
  25.                 }
  26.             } else {
  27.                 _loadFirstTrack();
  28.             }
  29.         } else {
  30.             resetMedia();
  31.         }
  32.     },
  33.     _playNextEnded = function() {
  34.         if( trackCount ) {
  35.             ++iTrack;
  36.             if ( iTrack < trackCount ) {
  37.                 _playTrack( iTrack );
  38.             } else {
  39.                 _loadFirstTrack();
  40.             }
  41.         } else {
  42.             resetMedia();
  43.         }
  44.     },
  45.     _playNext = function() {
  46.         if( trackCount ) {
  47.             ++iTrack;
  48.             if( iTrack < trackCount ) {
  49.                 if( isPlaying ) {
  50.                     _playTrack( iTrack );
  51.                 } else {
  52.                     _loadTrack( iTrack );
  53.                 }
  54.             } else {
  55.                 if( isPlaying ) {
  56.                     _loadFirstTrack();
  57.                 } else {
  58.                     iTrack = trackCount - 1;
  59.                     _loadTrack( iTrack );
  60.                 }
  61.             }
  62.         } else {
  63.             resetMedia();
  64.         }
  65.     },

Media Functions

  • lines 242-244: Create the mediaStart function to set or reset the current time of the loaded media.
  • lines 245-251: Create the resetMedia function to reset the currently loaded media.
  • lines 252-306: Create media selection functions based on control activation (event handlers).

Now we will complete the remainder of the code:

  1.     _loadFirstTrack = function() {
  2.         isPlaying = false;
  3.         iTrack = 1;
  4.         showPlay();
  5.         _loadTrack( iTrack );
  6.     },
  7.     _loadTrack = function(i) {
  8.         var title, album, artist;
  9.         title = tracks[i].title ? tracks[i].title : '&nbsp;';
  10.         album = tracks[i].album ? tracks[i].album : '&nbsp;';
  11.         artist = tracks[i].artist ? tracks[i].artist : '&nbsp;';
  12.         $title.html(title);
  13.         $album.html(album);
  14.         $artist.html(artist);
  15.         audio.src = tracks[i].file + ext;
  16.     },
  17.     _playTrack = function(i) {
  18.         _loadTrack(i);
  19.         audio.play();
  20.     },
  21.     getTracks = function(src) {
  22.         if($.isArray(src)) {
  23.             tracks = src;
  24.         } else {
  25.             tracks = [src];
  26.         }
  27.         _setTracks();
  28.     },
  29.     _setTracks = function() {
  30.         if( tracks && tracks.length ) {
  31.             if( tracks[0].file && tracks[0].file.length ) {
  32.                 tracks.unshift( {  } );
  33.             }
  34.             trackCount = tracks.length > 2 ? tracks.length : 0;
  35.             _loadFirstTrack();
  36.         }
  37.     };
  38.     $('#ap_container_1 .ap-controls').hover(
  39.         function() { $(this).addClass('ui-state-hover'); },
  40.         function() { $(this).removeClass('ui-state-hover'); }
  41.     );
  42. } else {
  43.     $('#ap_container_1').html('Your browser does not support the HTML5 audio tag.');
  44. }

We need a way to load the media, play the media, and get the media source.

  • lines 307-312: Create the _loadFirstTrack function to load the first track from the source.
  • lines 313-322: Create the _loadTrack function to load the indexed track from the source.
  • lines 323-326: Create the _playTrack function to load and play the indexed track.
  • lines 327-334: Create the getTracks function to get the media source.
    • If the source being passed into this function (src) is an array then assign it to the variable named tracks else we assume the src is an object so we place the object inside an array declaration and assign that to the variable named tracks.
    • Now that tracks is assigned, we get the number of items in the array and assign that to trackCount.

That completes the variable declaration portion of the code.

  • lines 344-347: Set a hover action on the controls. The function .hover is a jQuery library function.
  • line 318: Call the getTracks function. In this example we are passing in a JSON style object.
  • lines 348-350: Completes the if statement from line 34. If the HTML5 audio tag is not supported, we set and display the message. Then end the jQuery script.

Show a little style

The code is complete but it is not displaying correctly, right? We need to use a little CSS.
Create a CSS file and add the following:

  1. .ap-player,
  2. .ap-info-panel {
  3. 	position: relative;
  4. 	width: 300px;
  5. 	margin: 0 auto;
  6. 	padding: 20px;
  7. }
  8. .ap-info-panel {
  9. 	display: none;
  10. 	width: 320px;
  11. 	padding: 5px 10px 7px;
  12. }
  13. .ap-title,
  14. .ap-album,
  15. .ap-artist {
  16. 	margin: 0;
  17. 	padding: 0;
  18. }
  19. .ap-album {
  20. 	font-style: italic;
  21. }
  22. .ap-progress {
  23. 	position: absolute;
  24. 	top: 17px;
  25. 	left: 114px;
  26. 	right: 54px;
  27. }
  28. .ap-controls {
  29. 	position: relative;
  30. 	display: inline-block;
  31. 	margin: 2px;
  32. 	padding: 4px 0;
  33. }
  34. .ap-button {
  35. 	margin: 0 4px;
  36. }
  37. .ap-time {
  38. 	width: 50%;
  39. 	font-size: 0.8em;
  40. 	cursor: default;
  41. }
  42. .ap-current {
  43. 	position: relative;
  44. 	top: 0;
  45. }
  46. .ap-duration {
  47. 	position: absolute;
  48. 	top: 0;
  49. 	right: 2px;
  50. 	text-align: right;
  51. }
  52. .ap-mute,
  53. .ap-unmute {
  54. 	position: absolute;
  55. 	top: 18px;
  56. 	right: 20px;
  57. }

How To Use It

Now that we have the script and things look correct, you need to know a little bit more about how to actually use it. The getTracks function (line 318 above) is the only line you should have to modify to add whatever media you want for your web page. In the example above we passed in a JSON style object. We can also pass in an array. See:

getTracks( { "title":"The Title","album":"The Album","artist":"The Artist","file":"path/to/the/file" } );
Use a JSON style object

getTracks( { { "file":"path/to/file1" }, { "file":"path/to/file2" } ] );
Use an array of JSON style objects

You can use any combination of the following four options:

  1. "title"
  2. "album"
  3. "artist"
  4. "file"

The only required option is "file". It can be any location accessible by your web browser and there must be a .mp3 and a .ogg file of the same basename in the same location. For example; In the code, on line 318, you will notice that the "file" is listed as "/how_to/song". What that means is that on the web host server, relative to the location of the web page, there is a directory named "how_to" and in that directory there are two files. One named "song.mp3" and one named " song.ogg". If you do not have both file formats you are limiting your audience (see the related article "Convert Audio Files for HTML5 Playlist" for more information).

Download

Get the complete commented source code (zip file) here.

The purpose of this article was to show you how to Create a Custom HTML5 Audio Player interface. The related article "Create a Dynamic Playlist for HTML5 Audio (Advanced)" will expand on this article and show you how to use this custom audio player interface with a dynamic playlist.