Billy Bo the HTML Blob

Billy Bo Bob the HTML Blob.

An HTML, CSS and JavaScript Odyssey.

The Audio Element and object URLs - Let's make a little desktop mediaplayer

To see were this page is about, go to the video tutorial at youtube by clicking on this text here.

With the audio element you can add sound to your site and a complete specification can be found at www.w3.org/wiki/HTML/Elements/audio. The basic use of this element is quite simple as is shown in the code box below and its result below it.

<audio controls>
    <source src="media/messageBilly.ogg" type="audio/ogg">
    <source src="media/messageBilly.mp3" type="audio/mpeg">
    Your browser does not support the audio tag.
</audio>

But interesting use of the audio element is more complex. The sentence “Your browser does not support the audio tag” in the example code above, will be displayed if the audio element is not supported. This is done because the audio element is not supported in all browsers. On caniuse.com/#feat=audio and html5test.com you can see which browsers support the audio element. At this moment (01-02-2013), of the browsers with an usage share over 1% (see http://gs.statcounter.com/#browser_version_partially_combined-ww-monthly-201301-201301-bar for January 2013), only IE8 doesn't support this element. IE8 usage share is 11% and is shrinking with on average a half percent per month, so this problem is to stay with us for the next 20 months.

And there are more problems for this element. There is an abundance of codecs (codec = coder-decoder program) supported. In itself that is not a problem. The problem is that not one of these codecs is supported by all the browsers. If you want to support audio over all audio-element supporting browsers, you need at least two audio sources. The following browsers do support the following codecs.

Browser support for audio codecs/media type/extension
Browsers Audio Element WAV MP3 Ogg Vorbis MPEG-4 AAC WebM Vorbis
audio/wav or audio/wave audio/mpeg audio/ogg audio/mp4 audio/webm
.wav .mp3 .ogg, .oga .m4a .webm
Ogg Opus support is not mentioned, because at moment of writing it has not enough support. However ogg opus is included in the test
Android 4.0 Yes No Yes No No No
Chrome Yes Yes Yes Yes Yes Yes
FireFox Yes Yes No Yes No Yes
IE10 Yes No Yes No Yes No
IE9 Yes No Yes No Yes No
IE8 No No No No No No
iOS 6 Yes Yes Yes No Yes No
Maxton 4.0 Yes Yes Yes Yes Yes Yes
Opera Yes Yes No Yes No Yes
Safari 6 Yes Yes Yes No Yes No

In the next part, we will build a little media player with the audio element. For this we first need to test whether the audio element is supported or not. If the audio element is supported, then there is an entry in the window object for the Audio prototype or function. The test is in the box below.

var audioElementSupported = function () {
    return !!window.Audio;
};

Now we need to determine which audio codecs are supported.

var getSupportedFormats = function () {
   var audioElement = document.createElement("audio"),
       formats = [
           'audio/wav; codecs="1"'         ,
           'audio/mpeg;'                   ,
           'audio/ogg; codecs="vorbis"'    ,
           'audio/mp4; codecs="mp4a.40.2"' ,
           'audio/webm; codecs="vorbis"'   ,
           'audio/ogg; codecs="opus"'      ,
       ];

       return formats.filter(function (format) {
           var can = audioElement.canPlayType(format);
               return (can === "maybe" || can === "probably");
       });
};


Now we can find out if an audio file can be played, we want to read files from the computer. For this we use an input element of the type file and an object URL. With the file input, we can select the files and with the createObjectURL we can transform these files into URLs for the source attribute of the audio element. With the file list from the the input we make a little play-list.

You can check if your browser supports object URL by testing if URL or webkitURL have an entry in the window object. We will build in the ability to use object URL and to use FileReader's readAsDataURL to support also Opera. If window.FileReader exists, the file reader object is supported. Internet Explorer 9 and 8 don't support object URL and they also don't support FileReader, we will not be supporting them. If you want to support IE8 and 9, you could think to make use of silverlight or flash. Here we will not do that.

var objectUrlSupported = function () {
    return !!(window.URL || window.webkitURL);
};


var fileReaderSupported = function () {
    return !!window.FileReader;
};


We add the play-list, audio and the input elements to our site with the following HTML code.

<audio id="player3" controls></audio>
<form method="get" action="uploadMusic">
    <label for="file">Files:</label>
    <input type="file" form="fileEntry" id="file" name="file" multiple="multiple"/>
</form>

<ul id="playList1"></ul>

For reading the files, creating the play-list and doing all the necessary processing we will add the following JavaScript.

var player = function (obj) {
    var current   = 0,
        files     = null,
        objectUrl = null,
        player    = document.getElementById(obj.player),

    addHandler = function (element, type, handler) {
        if (element.addEventListener) {
            element.addEventListener(type, handler, false);
        } else if (element.attachEvent) {
            element.attachEvent("on" + type, handler);
        }
    },
    
    audioElementSupported = function () {
        return !!window.Audio;
    },
        
    formatSupported = function (format) {
        var can = player.canPlayType(format);
        return (can === "maybe" || can === "probably");
    },

    getSupportedFormats = function () {
        var formats = [
            'audio/wav; codecs="1"'         ,
            'audio/mpeg;'                   ,
            'audio/ogg; codecs="vorbis"'    ,
            'audio/mp4; codecs="mp4a.40.2"' ,
            'audio/webm; codecs="vorbis"'   ,
            'audio/ogg; codecs="opus"'      ,
        ];

        return formats.filter(function (format) {
            return formatSupported(format);
        });
    },
        
    setAcceptFileInput = function () {              //creates a string with the allowed media types 
        var acceptString = "";
    
        getSupportedFormats().forEach(function (format, i, array) {
           acceptString += format.split(";")[0] + (((i + 1) < array.length) ? ",": "");
        });
        return acceptString;
    },

    aSyncPlay = function (event) {         //function specific for handling readAsDataURL
        player.src = event.target.result;  
        player.play();
    },

    playFile = function (i) {
        if (i < files.length) {
            current = i;
            if (player.canPlayType(files[i].type)) {
                if (window.URL) {
                    player.src = window.URL.createObjectURL(files[i]);
                    player.play();
                } else {
                    fileReader.readAsDataURL(files[i]);
                }
            } else {
                playFile(i + 1);
            }
        }
    },
    
    ended = function () {
        if (window.URL) {
            window.URL.revokeObjectURL(this.src);
        }
        playFile(current + 1);
    },
        
    createPlayListEntry = function (i, file, fragment) {
        var li = document.createElement("li");

        li.innerText = file.name;
        li.textContent = file.name;
        if (player.canPlayType(file.type)) {                     //if playable color green
            li.className = "success";                            //otherways red and only add an eventlistner
            addHandler(li, "click", function () {                //if media type can be played
                if (window.URL && player.src) {
                    window.URL.revokeObjectURL(player.src);
                }
                playFile(i);
            });
        } else {
          li.className = "fail";
        }
        fragment.appendChild(li);
    },
        
    createPlayList = function (files, playList) {
        var fragment = document.createDocumentFragment(),
            ul = document.getElementById(playList),
            i;
                
        for (i = 0; i < files.length; i += 1) {
            createPlayListEntry(i, files[i], fragment);
        }
        ul.innerHTML = "";
        ul.appendChild(fragment);
    },
        
    create = function (obj) {
        var fileInput = document.getElementById(obj.file);
        
        fileInput.accept = setAcceptFileInput(obj.file);            //Set the supported mime-types as accepted.
        addHandler(fileInput, "change", function () {
            files = fileInput.files;
            if (window.URL && player.src) {
                window.URL.revokeObjectURL(player.src);
            }
            player.pause();
            player.src = "";
            if (obj.playList) {
                createPlayList(files, obj.playList);
            }
            playFile(0);
        });
        if (!window.URL) {                                     //if window.URL not defined we use readAsDataURL and need a callback for load event
          	fileReader = new FileReader();
            addHandler(fileReader, "load", aSyncPlay);
        }
        addHandler(player, "ended", ended);  //add a callback function for the end of the media
    },
        
    rePack = function (obj) {        //make sure the necessary fields are set.
       obj = obj || {};
       obj.player = obj.player || "player";
       obj.file = obj.file || "file";
       return obj;
    };

    window.URL = window.URL || window.webkitURL;
    if (audioElementSupported() && (window.URL || window.FileReader)) {           //only action if audio element and blob url or filereader are supported
        create(rePack(obj))
    }
};

    Now we have a functioning audio player, but the lay-out is unstable. The audio player is differently displayed in different browsers, also the file input button changes appearances in each browser and the play list determines the length of the displayed. So we need to make our own lay-out. We will do this by creating our own buttons and then let them react to the click event by calling a function. These buttons are the following:

    Functions for the audio player
    Function Button Name Description JavaScript Function
    Play Play Start playing the music selected AudioElement play
    Pause Pause Pauses the playing of the music or resumes the playing of the music AudioElement pause
    Prev Prev Goes to the previous number and plays it. Custom function
    Next Next Goes to the next number and plays it Custom function
    Up Vol+ Increases the volume. player.volume + 0.1
    Down Vol- Decreases the volume. player.volume - 0.1
    Mute Mute Toggles the sound on/off. player.muted = !player.muted
    Files Files Lets the file selector pop-up fileInput click
    Progress Bar Billy the Slider Shows the progress of the number and can be clicked for going to a time in the audio number. Billy the slider plus setting player currentTime

    We add to our progress bar (is Billy the slider, see article about The Progress Element) a listner for clicking and find the loction on the slider with var sliderValue = (event.pageX - event.currentTarget.getClientRects()[0].left) / event.currentTarget.clientWidth; and translate this for the audio player to player.currentTime = player.duration * sliderValue;

    The picture of Billy is at picture/billyTheSlider.png. If you don't want to see Billy, you can delete Billy as background picture in the CSS for #billyTheSlider, by removing the line background: url(picture/billyTheSlider.png) no-repeat;.

    You can use this audio widget by adding the following HTML to your site.

    <div>
        <div id="audioWidget">
        </div>
    </div>

    You can style the audio-widget with the following CSS, which you have to add to your site.

    .audioWidgetButton {
        display:            block;
        float:              left;
        border:             #CDF3CD solid 1px;
        margin:             2px;
        padding:            2px;
        width:              40px;
        text-align:         center;
    }
    .audioWidgetButton:hover, .billysPlayList:hover {
        cursor:             pointer;
        background-color:   rgb(240, 240, 240);
    }
    .billiesAttention {
        background-color:   #CDF3CD;
    }
    .billysPlayList {
        display:            block;
        border:             #CDF3CD solid 1px;
        padding:            2px;
        height:             45px;
        overflow:           hidden;
    }
    #billyTheSlider {
        position:           absolute;
        left:               0;
        top:                -6px;
        width:              30px;
        height:             30px;
        background:         url(picture/billyTheSlider.png) no-repeat; 
    #billyClock {
        display:            block;
        position:           relative;
        top:                50px;
        left:               -250px;
    }
    #billyWrapper {
        position:           relative;
        float:              left;
        width:              100%;
    }
    #container {
        border:             #CDF3CD solid 1px;
        color:              #2F4F4F;
        width:              400px;
        height:             400px;
    }
    #loadingprogress {
        display:            inline-block;
        float:              left;
        border-radius:      20px;
        height:             4px;
        width:              0;
    }
    #playListWrapper {
        overflow-x:         hidden;
        overflow-y:         scroll;
        height:             80%;
        margin-top:         50px;
        padding:            1px;
    }
    #progressbar {
        width:              380px;
        height:             4px;
        position:           relative;
        border:             1px solid #CDF3CD;
        margin:             10px;
        border-radius:      20px;
        background-color:   #cccccc;
        vertical-align:     middle;
    }
    #progressbar {
        cursor:             pointer;
    }

    Finally, if want to use the widget, you add the following code to your site.

    (function (parametersJson, undefined) {
        "use strict";
        var current       = 0,
            fileInput     = null,
            fileReader    = null,
            files         = null,
            container     = null,
            player        = null,
    
        billyProgress = function () {
            var progressBar     = document.getElementById("progressbar"),
                loadingProgress = document.getElementById("loadingprogress"),
                billyTheSlider  = document.getElementById("billyTheSlider"),
                barwidth        = progressBar.offsetWidth,
                privateMax      = 0,
                position        = 0,
                privateValue    = 0,
    
            setValuePrivate = function (value) {
                privateValue = value; 
                position = (privateMax !== 0) ? barwidth * privateValue / privateMax : 0;
                if (position  &lt;= barwidth) {
                    billyTheSlider.style.left   = position + "px";
                    loadingProgress.style.width = position + "px";
                } else {
                    billyTheSlider.style.left   = barwidth + "px";
                    loadingProgress.style.width = barwidth + "px";
                }
            },
    
            setMaxPrivate = function (max) {
                privateMax = max; 
                position = (privateMax !== 0) ? barwidth * privateValue / privateMax : 0;
                if (position  &lt;= barwidth) {
                  billyTheSlider.style.left   = position + "px";
                  loadingProgress.style.width = position + "px";
                } else {
                  billyTheSlider.style.left   = barwidth + "px";
                  loadingProgress.style.width = barwidth + "px";
                }
            },
    
            setBackgroundColorPrivate = function (backgroundColor) {
                progressBar.style.backgroundColor = backgroundColor;
            },
        
            setColorPrivate = function (backgroundColor) {
               loadingProgress.style.backgroundColor = backgroundColor;
            };  
        
           return {
               setValue            :   function (value)    { setValuePrivate(value);           },
               setMax              :   function (max)      { setMaxPrivate(max);               },
               getValue            :   function ()         { return privateValue;              },
               getMax              :   function ()         { return privateMax;                },  //this is the Progress Indicator.
               setColor            :   function (color)    { setColorPrivate(color);           },  //this is the Progress Bar Color.
               setBackgroundColor  :   function (color)    { setBackgroundColorPrivate(color); },
           };
        },
    
        audioElementSupported = function () {
            return !!window.Audio;
        },
            
        formatSupported = function (format) {
            var can = player.canPlayType(format);
            return (can === "maybe" || can === "probably");
        },
            
        getSupportedFormats = function () {
            var formats = [
                   'audio/wav; codecs="1"'         ,
                   'audio/mpeg;'                   ,
                   'audio/ogg; codecs="vorbis"'    ,
                   'audio/mp4; codecs="mp4a.40.2"' ,
                   'audio/webm; codecs="vorbis"'   ,
                   'audio/ogg; codecs="opus"'      ,
                ];
                
                return formats.filter(function (format) {
                    return formatSupported(format);
            });
        },
            
        setAcceptFileInput = function () {
            var acceptString = "";
            getSupportedFormats().forEach(function (format, i, array) {
                acceptString += format.split(";")[0] + (((i + 1)  &lt; array.length) ? ",": "");
            });
            return acceptString;
        },
    
        aSyncPlay = function (event) {
            player.src = event.target.result;
            player.play();
            billyProgress.setMax(100);
            billyProgress.setValue(0);
        },
    
        playFile = function (i) {
            var thisChild;
            
            if (i &gt;= 0 &amp;&amp; i &lt; files.length) {
                document.getElementById("playList").children[current || 0].classList.remove("billiesAttention");
                current = i;
                if (player.canPlayType(files[i].type)) {
                    if (window.URL) {
                        player.src = window.URL.createObjectURL(files[i]);
                        player.play();
                        billyProgress.setMax(100);
                        billyProgress.setValue(0);
                    } else {
                        fileReader.readAsDataURL(files[i]);
                    }
                    thisChild = document.getElementById("playList").children[i];
                    thisChild.classList.add("billiesAttention");
                    document.getElementById("playListWrapper").scrollTop =  ((i &gt; 2) ? i - 2 : 0) * thisChild.scrollHeight;   //only scroll after 3 numbers
                } else {
                    playFile(i + 1);
                }
            }
        },
    
            
        ended = function () {
            if (window.URL) {
                window.URL.revokeObjectURL(this.src);
            }
            playFile(current + 1);
        },
        
        createPlayListEntry =function (i, file, fragment) {
            var li = document.createElement("li"),
                fileName = file.name;
                
            fileName = fileName.substr(0, fileName.lastIndexOf('.')) || fileName;
            li.innerText = fileName;
            li.textContent = fileName;
            if (player.canPlayType(file.type)) {
                li.className = "success";
                li.className += " billysPlayList";
                li.addEventListener("click", function () {
                    if (window.URL && player.src) {
                         window.URL.revokeObjectURL(player.src);
                    }
                    playFile(i);
                });
            } else {
                li.className = "fail";
            }
            fragment.appendChild(li);
        },
            
        createPlayList = function (files) {
            var fragment        = document.createDocumentFragment(),
                ul              = document.getElementById("playList"),
                i;
                    
            for (i = 0; i &lt; files.length; i += 1) {
                createPlayListEntry(i, files[i], fragment);
            }
            ul.innerHTML = "";
            ul.appendChild(fragment);
        },
    
        play = function () {
            player.play();
        },
        
        pause = function () {
            player.pause();
        },
        
        prev = function () {
            playFile(current - 1);
        },
        
        next = function () {
            playFile(current + 1);
        },
        
        up = function () {
           if (player.volume  + 0.1 &lt;= 1) {
               player.volume += 0.1; 
           }
        },
        
        down = function () {
            if (player.volume - 0.1 >= 0) {
                player.volume -= 0.1; 
            }
        },
        
        mute = function () {
            player.muted = !player.muted;
        },
        
        filesClick = function () {
            fileInput.click();
        },
        
        createButtons = function (fragment){
            var buttons = [ {  id: "pause",    name: "pause",    func: pause      }, 
                            {  id: "prev",     name: "prev",     func: prev       },
                            {  id: "play",     name: "play",     func: play       },
                            {  id: "next",     name: "next",     func: next       },
                            {  id: "up",       name: "Vol +",    func: up         }, 
                            {  id: "mute",     name: "mute",     func: mute       },
                            {  id: "down",     name: "Vol -",    func: down       },  
                            {  id: "files",    name: "files",    func: filesClick }, 
                          ],
                ul = document.createElement("ul");
              
                buttons.forEach(function (button) {
                    var li = document.createElement("li");
                    
                    li.id          = button.id;
                    li.innertText  = button.name;
                    li.textContent = button.name;
                    li.className   = "audioWidgetButton";
                    li.addEventListener( "click", button.func, false);
                    ul.appendChild(li);
                });
            ul.id = "buttons"; 
            fragment.appendChild(ul);
            return fragment;
        },
        
    
        seekBilly = function (event) {
            var sliderValue = (event.pageX - event.currentTarget.getClientRects()[0].left) / event.currentTarget.clientWidth;
            player.currentTime = player.duration * sliderValue;
        },
        
        addBillyTheSlider = function (fragment) {
             var billyWrapper    = document.createElement("div"),
                 progressbar     = document.createElement("div"),
                 loadingprogress = document.createElement("div"),
                 billyTheSlider  = document.createElement("div"),
                 billyClock      = document.createElement("code");
    
             billyTheSlider.id   = "billyTheSlider";
             loadingprogress.id  = "loadingprogress";
             progressbar.id      = "progressbar";
             billyWrapper.id     = "billyWrapper";
             
             progressbar.appendChild(billyTheSlider);
             progressbar.appendChild(loadingprogress);
             billyWrapper.appendChild(progressbar);
             fragment.appendChild(billyWrapper);
             progressbar.addEventListener("click", seekBilly, false);
             
             billyClock.id = "billyClock";
             billyClock.innerText   = "time: 00:00/00:00";
             billyClock.textContent = "time: 00:00/00:00";
             fragment.appendChild(billyClock);
    
             return fragment;
         },
    
        addElements = function (fragment) {
            var playListWrapper = document.createElement("div"),
                ul = document.createElement("ul");
            
            playListWrapper.id = "playListWrapper";
            ul.id              = "playList";
            
            playListWrapper.appendChild(ul);
            fragment.appendChild(playListWrapper);
            fragment.appendChild(fileInput);
            return fragment;
        },
    
        createSrcDoc = function (parametersJson, fragment) {
            createButtons(fragment);
            addBillyTheSlider(fragment);
            addElements(fragment);
            return fragment;
        },
        
        rePack = function (parametersJson) {
            parametersJson.id    = parametersJson.id || "audioWidget";
            return parametersJson;
        },
        
        playFiles = function () {
            files = fileInput.files;
            player.pause();
            if (window.URL &amp;&amp; player.src) {
                window.URL.revokeObjectURL(player.src);
            }
            player.src = "";
            createPlayList(files);
            playFile(0);
        },
        
        pad = function (n) {
            return (n &lt; 10) ? ("0" + n) : n;
        },
        
        setClock = function (duration, currentTime) {
            var billyClock = document.getElementById("billyClock"),
                value,
                max,
                minutesValue,
                minutesMax,
                secondsValue,
                secondsMax,
                text;
    
            value = (currentTime) ? currentTime / 60 : 0;
            max   = (duration)    ? duration / 60    : 0;
            
            minutesValue           = Math.floor(value);
            minutesMax             = Math.floor(max);
            secondsValue           = Math.floor(((value - minutesValue) * 60));
            secondsMax             = Math.floor(((max - minutesMax) * 60));
            text                   = "time: " + pad(minutesValue) + ":" + pad(secondsValue) + "/" + pad(minutesMax) + ":" + pad(secondsMax);
            billyClock.innerText   = text;
            billyClock.textContent = text;
        },
    
        setBillyTheSliderValue = function () {
            billyProgress.setMax(player.duration || 100);
            billyProgress.setValue(player.currentTime || 0);
            setClock(player.duration, player.currentTime);
        },
        
        errorMessage = function (parametersJson){
            var hookDiv = document.getElementById(parametersJson.id);
    
            hookDiv.innerHTML += "<p class='fail'>Oops, something went wrong!</p>";
    
            if (!audioElementSupported()) {
                hookDiv.innerHTML += "<p class='fail'>Your browser does not support the audio element. If you want to know which browsers support the audio element, see <a href='http://caniuse.com/#feat=audio' target='_blank'>caniuse.com/#feat=audio</a>.</p>";
            }
            if (!window.URL) {
                hookDiv.innerHTML += "<p class='fail'>Your browser does not support the URL Object. If you want to know which browsers support the object urls, see <a href='http://caniuse.com/#feat=bloburls' target='_blank'>caniuse.com/#feat=bloburls</a>.</p>";
            }
            if (!window.FileReader) {
                hookDiv.innerHTML += "<p class='fail'>Your browser does not support the FileReader. If you want to know which browsers support the filereader, see <a href='http://caniuse.com/#feat=filereader' target='_blank'>caniuse.com/#feat=filereader</a>.</p>";
            }
        },
    
        build = function (parametersJson) {
            var hookDiv = document.getElementById(parametersJson.id);
            
            player    = document.createElement("audio"), 
            fileInput = document.createElement("input"),
            
            fileInput.type             = "file";
            fileInput.style.visibility = "hidden";
            fileInput.multiple         = "multiple";
            fileInput.accept           = setAcceptFileInput();
            fileInput.addEventListener("change", playFiles, false);
            
            container    = document.createElement("div");
            container.id = "container";
            
            hookDiv.appendChild(container);
            
            container.appendChild(createSrcDoc(parametersJson, document.createDocumentFragment()));
            
            billyProgress = billyProgress();
            billyProgress.setValue(0);
            billyProgress.setMax(100);
            billyProgress.setColor("#CDF3CD");
            billyProgress.setBackgroundColor("white");
    
            if (!window.URL) {
                fileReader = new FileReader();
                fileReader.addEventListener("load", aSyncPlay, false);
            }
            player.addEventListener("ended", ended, false);
            setInterval(setBillyTheSliderValue, 250);
        };
        
        if (window.addEventListener) {
            window.addEventListener( "load", function () {
                window.URL = window.URL || window.webkitURL;
                if (audioElementSupported() && (window.URL || window.FileReader)) {
                    build(rePack(parametersJson));
                } else {
                    errorMessage(rePack(parametersJson));
                }
            }, false);
        } else {
            window.onload = function () {
                errorMessage(rePack(parametersJson));
            };
        }
    }({
        id: "audioWidget",
    }));

    On the android audio isn't played, probably because of permissions. The object url is created and the audio object simply doesn't read it. See also Issue 41995: html5 audio tag in webview cannot play local audio files.

    We have created an audio widget for HTML5 browsers and we have seen some of the problems (support, codecs, different appearances...) that come with the HTML5 audio element. However, nowadays cross browser support for flash is problematic to. Some of the problems were caused by the use of local files, so in the most common uses they will not appear.