Audio duration NaN on certain page request action

1.7k views Asked by At

I have been trying to create my custom media player using HTML5 and Jquery.

I have followed different approaches and ran into some trouble based on my way of refreshing the page.

First Case

$(document).ready(function(){
    duration = Math.ceil($('audio')[0].duration);
    $('#duration').html(duration);
});

In this case, the duration returns NaN when I redirect the page to the same URL by pressing the ENTER key in the address bar. However, it works completely fine when I refresh using the reload button or by pressing the F5 button.

Second Case

I read in some answers that loading duration after the loadedmetadataevent might help. So I tried the following:

$(document).ready(function(){
    $('audio').on('loadedmetadata', function(){
        duration = Math.ceil($('audio')[0].duration);
        $('#duration').html(duration);
    });
});

Surprisingly, in this case, the inverse of the first case happened. The duration gets displayed completely fine in the case of a redirect, i.e., pressing ENTER while in the address bar. However, in the case of refreshing using the F5 button or the reload button, the duration doesn't get displayed at all, not even NaN which led me to believe that the code doesn't get executed at all.


Further reading suggested this might be a bug within the webkit browsers but I couldn't find anything conclusive or helpful.

What could be the cause behind this peculiar behavior? It'd be great if you could explain it along with the solution to this problem.

Edit: I am mainly looking for an explanation behind this difference in behavior. I would like to understand the mechanism behind rendering a page in the case of redirect and refresh.

4

There are 4 answers

4
Casey Chu On BEST ANSWER

It sounds like the problem is that the event handler is set too late, i.e. the audio file has loaded its metadata before the document is even ready.

Try setting the event handler as soon as possible by removing the $(document).ready call:

$('audio').on('loadedmetadata', function(){
    duration = Math.ceil($('audio')[0].duration);
    $('#duration').html(duration);
});

Note that this requires that the <script> tag be after the <audio> tag in the document.

Alternatively, you can tweak your logic slightly, so that the code that updates the duration always runs (but fails gracefully if it gets a NaN):

function updateDuration() {
    var duration = Math.ceil($('audio')[0].duration);
    if (duration)
        $('#duration').html(duration);
}

$(document).ready(function(){
    $('audio').on('loadedmetadata', updateDuration);
    updateDuration();
});
1
Denys Denysiuk On

According to w3 spec this is standard behavior when duration returns NaN. So I suggest use durationchange event:

$('audio').on('durationchange', function(){
    var duration = $('audio')[0].duration;
    if(!isNaN(duration)) {
        $('#duration').html(Math.ceil(duration));
    }
});

NOTE: This code (and your too) will not work correct in case if you have more than one audio element on page. Reason is that you listen events from all audio elements on page and each element will fire own event:

$('audio').on('durationchange', function(){...});

OR

You can try:

    <script>
       function durationchange() {
          var duration = $('audio')[0].duration;
          if(!isNaN(duration)) {
              $('#duration').html(Math.ceil(duration));
          }
        }
    </script>

    <audio ondurationchange="durationchange()">
         <source src="test.mp3" type="audio/mpeg">
    </audio>
2
Rycochet On

Lovely code examples and stuff from people - but the explanation is actually very simple.

If the file is already in the cache then the loadedmetadata event will not fire (nor will a number of other events - basically because they've already fired by the time you attach your listeners) and the duration will be set. If it's not in the cache then the duration will be NaN, and the event will fire.

The solution is sort of simple.

function runWhenLoaded() { /* read duration etc, this = audio element */ }

if (!audio.readyState) { // or $audio[0].readyState
    audio.addEventListener("loadedmetadata", runWhenLoaded);
    // or $audio.on("loadedmetadata", runWhenLoaded);
} else {
    runWhenLoaded.call(audio);
    // or runWhenLoaded.call($audio[0]);
}

I've included the jQuery alternatives in the code comments.

4
Julien Grégoire On

Note that behaviors will differ from one browser to another. On Chrome, you have different type of loading. When resources are not in cache, it will fetch either the complete file (for js or css for example), either a part of the file (mp3 for example). This partial file contains metadata that allows browser to determine duration and other data such as the time it'll take to download whole file at this rate, trigerring for example canplay or canplaythrough events. If you look at network usage in you dev console, you'll see that the HTTP status code will be either 200 (succesful load) or 206(partial load - for mp3 for example).

When you hit refresh, elements are checked to see if they changed. HTTP status will then be 304, meaning file hasn't been modified. If it hasn't changed and is still in browser cache, then it won't be downloaded. The call to determine if it has or not changed comes from the server providing the file.

When ou simply click enter in adress bar, it's automatically taken from cache, not validating online. So it's much faster.

So depending on how you call or refresh your page (either simmple enter, refresh or complete refresh without cache), you'll have big differences on the moment you get the metadata from your mp3. Between taking the metadata from cache directly vs making a request to a server, the difference can be a few hundreds milliseconds, which is enough to change what data is available at different moment.

That being said, listening to loadedmetada should give consistent result. This event is triggered when the data with duration information is loaded, so whatever way the page is loaded, it shouldn't matter if that called is properly made. At this point you have to consider maybe some interference from other elements. What you should do is follow your audio through various events to get exactly where its at at different moments. So in you document ready you could add listeners for different event and see where the problem occurs. Like this:

$('audio')[0].addEventListener('loadstart', check_event)
$('audio')[0].addEventListener('loadeddata', check_event)
$('audio')[0].addEventListener('loadedmetadata', check_event)//at this point you should be able to call duration
$('audio')[0].addEventListener('canplay', check_event) //and so on

function check_event(e) {
   console.log(e.target, e.type)
}

You'll see that depending on the way you refresh, these events can come at different moments, maybe explaining inconsistencies in your outputs.