Consistent Event Firing With HTML5 Video

Introduction

If you play around with the HTML5 <video> element for any significant length of time, you may well run into a problem with inconsistently firing events: sometimes you don't seem to get some events, and sometimes it behaves differently depending on whether you're testing locally or over the network.

This article explores the problem, and puts forward some solutions.

Why does it happen?

The cause of this problem is that there is probably a race condition in effect: the browser fires the events before you have had a chance to set up the event listeners for them. Consider the example below:

<video src="test.webm" id="video"></video>
<script>
var video = document.getElementById('video');
video.onloadedmetadata = function(e) {
  alert('Got loadedmetadata!');
}
</script>

If you test locally or over a fast network, the video will probably load very quickly, which means that the browser will fire loadedmetadata before the script that sets the event listener has run. If on the other hand you test over a slow network, where the video takes a bit longer to load, the script might run before the browser fires the event. Whether you get the event simply depends on network conditions, whether the video is cached, and possibly even where the TCP packet boundaries happen to be in the markup. This can also be affected if you use <source> elements instead of a src="" attribute, and also affects other events - like loadstart, loadeddata, canplay, etc.

A permanent solution in the browser?

Surely this is a bug - isn't there a way to fix this problem in the browsers? Not really - browsers want to load the video as soon as possible, and cannot know when or if you are going to set an event listener for a particular event. So instead, if there is for example no event listener for loadedmetadata when the browser has loaded the metadata of the video, it just throws away the event.

How about a fix then?

To avoid this brittleness, you should make sure that you have set the event listeners before the browser has a chance to fire them. You can either use event listeners as attributes on the <video> element, as follows:

<video src="test.webm" onloadedmetadata="alert('Got loadedmetadata!')"></video>

Or you can create the <video> element in the same script that sets the event listeners:

<script>
var video = document.createElement('video');
video.onloadedmetadata = function(e) {
  alert('Got loadedmetadata!');
}
video.src = 'test.webm';
document.body.appendChild(video);
</script>

Both of these solutions are robust regardless of network conditions - you will always get the loadedmetadata event since the event listener is registered up front.

If you want to avoid inline JavaScript, but don't want to write the <video> element with script (you want users without JS to be able to see the video with browser-default controls while users with script get the sexy enhanced video player), you can use a capturing event listener on an ancestor object by setting the last argument of addEventListener to true. This causes the event listener to be invoked in the capturing phase of the event. Events first go through the window object, then the document object, then the root element and down the chain of ancestors of the target element, then the target element itself, and finally, if the event is a bubbling event, it goes back through the target's ancestors again. The events on media elements are not bubbling events, however, which is why a capturing event listener is needed to listen for a media element event on an ancestor object.


<head>
<script>
window.addEventListener('loadedmetadata', function(e) {
  alert('Got loadedmetadata!'); }, true);
</script>
</head>
<body>
<video src="test.webm"></video>

The event listener will fire for any media element in the page; to know which media element fired the event, use e.target.