Essential Audio and Video Events for HTML5

Tweet

The <video> and <audio> elements provide a comprehensive range of events. While some are quite straightforward, like the self-explanatory "play" event, others can be rather more tricky to understand, especially the "progress" event.

So let’s examine some of the most important media events, looking at when and how they fire and what properties are relevant to them. We’ll also try to navigate the quirks of their behavior in current browsers (well, you didn’t think they’d all be the same, did you?).

(For reference testing, I’ll be using the latest public versions of the most common browsers — Opera 12, Chrome 28, IE10, Firefox 22, Safari 5 (desktop) and Mobile Safari 6 (iOS). So wherever a browser is referred to only by name (e.g. Opera) it means this latest version.)

Playback Events

The playback events are those which fire in response to playing or pausing the media. These events are quite straightforward.

The "play" and "pause" events fire when the media is played or paused (respectively), but there’s also an "ended" event which fires when the media reaches the end — either because ordinary playback has finished, or because the user manually “seeked” that far.

There are media functions that correspond with the first two events — unsurprisingly called play() and pause(). There are also two media properties that correspond with the last two events — the .paused property is true by default, or whenever the media is paused, while the .ended property is false by default, but then becomes true when playback reaches the end (i.e. at the same time as the "ended" event fires).

However there is a significant anomaly here in Opera, Safari and IE10, which is that the .paused flag remains false when the media has ended (yet logically, it should be true since the media is no longer playing). A practical upshot of this is that a simple play/pause button handler like this would fail in that situation (i.e. the button would do nothing at all):

button.addEventListener('click', function(e)
{
  if(media.paused)
  {
    media.play();
  }
  else
  {
    media.pause();
  }

}, false);

But you can fix this quirk quite easily, by firing the pause() method manually in response to the "ended" event:

media.addEventListener('ended', function(e)
{
  media.pause();

}, false);

Firefox and Chrome already fix this internally, and in exactly the same way — by firing a "pause" event just before the "ended" event.

Loading Events

The loading events are those which fire in respect of loading (or failing to load) media data. The prevalence of these events depends on the loading state of the media, i.e. whether the preload attribute is used and/or whether the media is already cached.

The first to fire in all cases is the "loadstart" event, which means that the browser has begun to look for data. But that’s all it means — it doesn’t mean any data has actually loaded, or that the media resource even exists.

If the preload attribute has the value "none", then the "loadstart" event is the only one that will fire before playback begins. Whereas if the preload attribute has the value "metadata" or "auto", then two more events will fire quite soon, which are "progress" and "loadedmetadata". (Without preloading these events will still fire, but not until playback begins.)

The "progress" event is rather complex, so we’ll look at that separately in the next section, but the "loadedmetadata" event is straightforward, as it simply means that the browser has loaded enough meta-data to know the media’s .duration (as a floating-point number, rather than its default value of NaN).

Of course the "loadedmetadata" event will only fire at all if the media is able to load — if it fails (for example, if the src returns a 404), then the media will instead produce an "error" event, and no further playback will be possible.

Here again we encounter some important browser variations. In Mobile Safari the preload settings are intentionally not implemented, so all values for that attribute behave the same as if it were "none". In IE10 by contrast, the media meta-data is always loaded by default, so a preload value of "none" behaves the same as if it were "metadata".

After "loadedmetadata" has fired, the next significant event is "canplay", which the browser will fire to indicate when enough data has loaded for it to know that playback will work (i.e. that it can play). If preload is "auto" then the "canplay" event will fire after a couple of seconds of data has loaded; if preload is "metadata" or "none" it won’t fire until playback has begun. The one exception to this rule is Chrome, which always fires "canplay" during initial preload, even if it’s only meta-data.

There’s also a secondary event called "canplaythrough", which the browser should fire when it estimates that enough media data has loaded for playback to be uninterrupted. This is supposed to be based on an estimation of your connection speed, and so it shouldn’t fire until at least a few seconds’ of data has been preloaded.

However in practise, the "canplaythrough" event is basically useless — because Safari doesn’t fire it at all, while Opera and Chrome fire it immediately after the "canplay" event, even when it’s yet to preload so much as a quarter of a second! Only Firefox and IE10 appear to implement this event correctly.

But you don’t really need this event anyway, since you can monitor the "progress" event to determine how much data has been preloaded (and if need be, calculate the download speed yourself):

The Progress Event

The "progress" event fires continually while (and only while) data is being downloaded. So when preload is set to "none", it doesn’t fire at all until playback has begun; with preload set to "metadata" it will fire for the first few seconds, then stop until playback begins; with preload set to "auto" it will continue to fire until the entire media file has been downloaded.

But for all preload settings, once playback has begun, the browser will proceed to download the entire media file, firing continual "progress" events until there’s nothing left to load, which continues in the background even if the video is subsequently paused.

The data itself is represented by a set of time-ranges (i.e. discreet portions of time), and it’s crucial to understand how these work before we can make use of the "progress" events.

When the media first starts to load, it will create a single time-range representing the initial portion. So for example, once the first 10 seconds’ of data has been loaded, the time-range could be represented as an array of start and end times:

[0,10]

However it’s possible (in fact very likely) for multiple time-ranges to be created. For example, if the user manually seeks to a time beyond what’s already been preloaded, the browser will abandon its current time-range, and create a new one which starts at that point (rather than having to load everything in between, as basic Flash players do).

So let’s say the user jumps forward two minutes and continues playback from there, then once another 10 seconds have preloaded, we’d have two ranges, which we could represent like this:

[
  [0,10],
  [120,130]
]

If the user were then to jump back again, to a time mid-way between the two ranges, then another (third) range would be created:

[
  [0,10],
  [60,70],
  [120,130]
]

Then once the end of that range reached the starting point of the final one, the ranges would be merged together:

[
  [0,10],
  [60,130]
]

The arrays in those examples are just representations, to help explain the concept — they’re not how time-range data actually appears; to get the data in that format we must compile it manually.

The media has a .buffered object that represents the time-ranges. The .buffered object has a .length property to denote how many ranges there are, and a pair of methods called start() and end() for retrieving the timing of an individual range.

So to convert the buffered data into those two-dimensional arrays, we can compile it like this:

var ranges = [];
for(var i = 0; i < media.buffered.length; i ++)
{
  ranges.push([
    media.buffered.start(i),
    media.buffered.end(i)
    ]);
}

And this is what we do with the "progress" events:

media.addEventListener('progress', function()
{
  var ranges = [];
  for(var i = 0; i < media.buffered.length; i ++)
  {
    ranges.push([
      media.buffered.start(i),
      media.buffered.end(i)
      ]);
  }
}, false);

Ultimately, we can use that data to create something more user-friendly — like a visual progress-meter, as the following demo shows. It’s simply a bunch of positioned <span> inside a containing <div> (we can’t use the <progress> element because it doesn’t support multiple ranges):

There are a few notable browser quirks with "progress" events and buffered data. The first is a difference in the .buffered data when loading from the start — whereas most browsers create a single time-range (as described at the start of this section), Opera will create two ranges, with the first being as expected, and the second being a tiny fragment of time right at the end (roughly the last 200ms). So if the media were two minutes long and the first 10 seconds had loaded, the ranges would be something like this:

[
  [0,10],
  [119.8,120]
]

Another caveat is that Mobile Safari doesn’t retain the data for multiple ranges — it discards all but the active range (i.e. the range that encompasses the current playback position). This is clearly intentional behavior, designed to minimize the overall amount of memory that media elements consume. So to use the earlier example again, where the user jumps forward two minutes, the resulting buffered data would still only contain a single range:

[
  [120,130]
]

Both of these quirks are worth knowing about, but they won’t usually make much difference, as far as development is concerned. However another, far more significant quirk, is the behavior of browsers in cases where the entire media file has already been preloaded. In this case, most browsers will fire a single "progress" event, containing a single time-range that represents the entire duration. However Opera and IE10 don’t provide this progress data — Opera fires a single event in which the buffer has no ranges (i.e. .buffered.length is zero), while IE10 doesn’t fire any "progress" events at all.

In the case of the visual progress-meter, this would mean that the meter stays empty, instead of being filled. But it’s simple to fix nonetheless, using an additional "loadedmetadata" event — because once that event fires in these browsers, the .buffered data does now represent the full media duration.

Timing Events

The last thing we’ll look at briefly is the media "timeupdate" event, which fires continually while the media is playing. You would use this event to synchronize other things with media playback, such as creating manual captions, highlighting the active line in a transcript, or even for synchronising multiple media sources — something I looked at in an earlier article: Accessible Audio Descriptions for HTML5 Video.

The frequency at which the "timeupdate" event fires is not specified, and in practise it varies widely among different browsers. But as an overall average it amounts to 3–5 times per second, which is accurate enough for most synchronisation purposes.

As far as I know, there are no browser bugs or quirks with this event. Makes a nice change, hey!

Afterword

This article doesn’t include every possible media event — there are other playback and seeking events, events for advanced network states, and even one which fires when the volume changes. But I’ve covered what I think are the most important — enough for most of the simple scripting you might want to do with video and audio, and enough to build a basic custom interface.

Here’s a final reference demo to help you get a feel for these media events. It creates a dynamic log of the playback and progress events we’ve discussed, showing timings and related property data to accompany each event:

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Jarrod Payne

    Awesome article! What is your favorite media element abstraction library to help with all of the browser pain?

    • http://www.brothercake.com/ James Edwards

      I would heartily recommend MediaElement.js — http://mediaelementjs.com/

      That library has saved me a whole heap of pain! It fixes loads of quirks and bugs in native implementations, and also provides a backup Flash player, both of which are ratified in a set of methods and properties which mimic the native media API (ie. play() and pause() methods, timeupdate and progress events, and so forth).

      It’s also modular, so you can pick and choose which bits of functionality you want, from single functions all the way up to a complete custom player.

  • http://efren-martinez.pro Efren Martinez

    Great ! very helpful , thanks !!

  • http://scribie.com Rajiv

    Is there some sort of limit on how much data can be loaded at one go? When I try to load a file which is 1 hour long, the loading will stop around 12 minute mark first. Loading starts again when the play head reaches nearby that mark. To force the full file to load, I have to play the last second of the file. But that again starts seeking and it blocks play till the entire file has been loaded.

    • http://www.brothercake.com/ James Edwards

      You don’t say which browser you’re referring to?

      I haven’t seen the behaviour you described, but what I would imagine is going on there is some kind of memory management, so that the browser doesn’t have to keep too much data in RAM. Remember that video files are very large — up to 10MB per minute for a high quality movie — so an hour of that would be more than half a gigabyte, all of which would have be kept in RAM.

      But if you seek far ahead, the browser shouldn’t load everything in between, it should start a new time range and load only from the point you jumped to. Mostly that works, but from time to time I have seen erroneous behavior in Webkit browsers, where time-ranges start loading from completely the wrong place (even sometimes, from past the point you jumped to, which means the video never resumes playback because it has no cached data for that point in time, it just keeps loading past it to the end!)

  • ioconnor

    I listen to a lot of podcasts and wonder if it possible to use html5 controls where I can enter the IRL of the podcast and then as I listen have:
    1) A button on to rewind 10 seconds, 30 seconds, and possibly a minute.
    2) A begin marker where I can start replaying from. Sometimes I’m slow and need to listen to something over a few times before I finally get it.

    Are these sort of things possible with html5?

  • http://www.mathewporter.co.uk Mathew Porter

    Great post guys, nice to know for adding functionality to html5 audio and video, will give a much better user experience.