1

I'm working on an audio player using the HTML5 <audio> element and am trying to iron out a few issues cross-browser.

This player is configured to switch between audio tracks when a link for a different track is clicked. Internally, it pauses the <audio> element, sets its currentTime to 0 (as explained in HTML5 audio not playing multiple times in Android 4.0.4 device Native Browser) and removes it from the DOM before a new <audio> element is created with the new source track specified. The play() method is called on the element to load the track and start playing.

On desktop browsers, an iPhone and iPad, this works fine. It also works on a colleague's Samsung Galaxy S3 Mini. However on a different test phone, a full-size Samsung Galaxy S3, the playback of subsequent tracks fails - the track won't even play and it shows in the playback bar as having completed. This is also the case on the stock Android 4.1.2 browser on my Motorola Xoom tablet.

The first track specified on page load always works fine.

After adding a few debugging statements among the JavaScript code, particularly the timeupdate callback function, I have found that in the cases where playback fails, the <audio> element's duration has a value of 1 in the two instances when timeupdate is raised. Normally, the duration is reported as anything from 350 to just over 1000 for a 3-4 min track. This explains why the playback bar immediately showed the newly-loaded track as having completed playback.

I'm thinking that the subsequent track isn't being loaded properly; even if it has been previously loaded and cached on the phone, surely it must be able to be loaded correctly. All tracks are MP3s and are able to be played in the respective browsers.

What am I missing here? Any idea what the problem is and how to get this to work?

Many thanks.

More Info: I've hooked up other events to the <audio> element to trace what may be happening:

  • progress (with a report on the buffered ranges)
  • loadedmetadata
  • canplay
  • error

On desktop (where this works correctly), the events are as follows: timeupdate (with NaN duration), progress x3 (0 buffered ranges), loadedmetadata, canplay, timeupdate with duration of 121.172368, progress with 1 buffered range [0, 11.507715], followed by a sequence of timeupdate and progress events as the track continues to play. The iPhone has a similar sequence of events.

On the Samsung S3, I get the following events: timeupdate with a duration of 1, followed by progress (0 buffered ranges), loadedmetadata, canplay, and finally a timeupdate with duration still at 1.

This page has been very useful in explaining the events and data structures: http://www.sitepoint.com/essential-audio-and-video-events-for-html5/

Community
  • 1
  • 1
d_mcg
  • 788
  • 2
  • 11
  • 19
  • Why are you using the play time to wait to remove the elements instead of using the `ended` event which is fired after the sound finishes playing? – megawac Oct 21 '13 at 02:12
  • I'm not switching the track on `timeupdate`; I've updated the question to try and clarify this. Since the tracks are several minutes long, it is likely that site visitors would switch between tracks part-way through playback by clicking on the appropriate track link. I already have the `ended` event wired up to handle that case, though this event doesn't arise in the scenario I'm referring to. – d_mcg Oct 21 '13 at 02:43
  • I was looking over how they get the duration in soundmanager2 but it looks like they didn't have a real solution to the problem either: https://github.com/scottschiller/SoundManager2/blob/master/script/soundmanager2.js#L2833 – megawac Oct 21 '13 at 04:41
  • Can you do a check to see if the duration is invalid and then get the duration via ajax from the server? – megawac Oct 21 '13 at 04:45
  • I was under the impression that the `duration` was calculated on the `loadedmetadata` event from the MP3 itself. This seems to be browser-dependent as well - the same 3:24-long track has a duration of 205.4008 on the iPhone, but 946.4950 on the Galaxy S3. – d_mcg Oct 21 '13 at 20:29
  • I've read that some browsers don't even calculate the duration until the sound has been played once, so predetermining the metadata is probably the best bet – megawac Oct 21 '13 at 20:46

2 Answers2

2

Well I finally solved it:

  • Firstly, I updated the audio implementation to reuse the same <audio> element when switching tracks, making sure that I paused the audio before changing the src, etc. This may or may not have had any positive effect, though it was a way to guarantee that we didn't have too many <audio> elements potentially floating around in memory, especially if some unknown phone can only handle one instance at a time.
  • Before switching tracks, I added a guard clause to stop any currently-playing audio and ensure that neither pause() nor setting currentTime caused an error if no audio had been loaded:

    if (audioPlayerElement.readyState > 0)
    {
        audioPlayerElement.pause();
        audioPlayerElement.currentTime = 0;
    }
    
  • Since subsequent tracks are auto-played, the ordering of statements needed fixing.

The misreporting of the track duration was a symptom of a larger issue in the way the <audio> element was being used. Previously, I had initialised the audio player as follows:

  1. Create the <audio> element / set the src to the new URL of the MP3 track
  2. Call load() on the <audio> element
  3. Bind the timeupdate and ended events
  4. Since this was an auto-play, added the autoplay attribute to the <audio> element
  5. Since this was an auto-play, called play() on the <audio> element

(The binding of loadedmetadata, canplay, progress events for debug tracing was performed between steps 1 and 2 above.)

The problems that I found with this sequence were:

  • Calling play() is redundant if the autoplay attribute is set; and
  • Adding the autoplay attribute should happen before load() is called, since load() automatically starts playing the track once enough data has been buffered.

Thus, the final steps that work are:

  1. Set the src attribute of the <audio> element to the new URL of the MP3 track
  2. Bind the timeupdate and ended events
  3. Since this is an auto-play, add the autoplay attribute to the <audio> element
  4. Call load() on the <audio> element

The play() function only applies when clicking the play/pause button or starting playback from a particular position on the track bar.

Finally, it's worth noting that on iOS devices and some Android devices (depending on browser), auto-play is ignored if the player is initialised to start playing on page load. On such devices, auto-play will only work if the execution can be traced back to a click event (or similar). Bear this in mind when updating visuals to indicate 'playing' state - the canplay event handler seems a good place to do this.

d_mcg
  • 788
  • 2
  • 11
  • 19
0

I'm going to fully answer your question because it is useful for me as well - if you have a better solution please let me know :)

I'm going to be keeping a hash of all the metadata I'll need for each sound. This hash is probably going to be just a json response from the server - eg:

app.sounds.metadata = {
    "enter the sandman": {
        "artist": "Metaliica",
        "album": "Metallica.",
        "description": "Lorem ipsum dolor sit amet."
        "genre": "metal",
        "duration": 19920,
        "playcount": 312
    },
    "piano man": {
        "artist": "Billy Joel",
        "album": "You're my home",
        "description": "Lorem ipsum dolor sit amet."
        "genre": "Soft rock",
        "duration": 16200,
        "playcount": 135,

    }
}

And my audio elements will be declared such that:

<audio data-sound="piano man">
    <source src="piano_man.ogg" type="audio/ogg">
    <source src="piano_man.mp3" type="audio/mpeg">
</audio>

Now when I'm using my sound I can just use:
var html5offset = 1000

var meta = app.sounds.metadata[sound.getAttribute("data-sound")];
var soundduration = meta.duration || sound.duration*html5offset;
megawac
  • 10,953
  • 5
  • 40
  • 61
  • Thanks for the help. I found that the misreported duration was a symptom of deeper issues - see my answer for details. – d_mcg Oct 24 '13 at 04:56