Safari Reference Library Apple Developer
Search

Controlling Media with JavaScript

Because the <audio> and <video> elements are part of the HTML5 standard, there are standard JavaScript methods, properties, and DOM events associated with them.

There are methods for loading, playing, pausing, and jumping to a time, for example. There are also properties you can set programmatically, such as the src URL and the height and width of a video, as well as read-only properties such as the video’s native height. Finally, there are DOM events you can listen for, such as load progress, media playing, media paused, and media done playing.

This chapter shows you how to do the following:

For a complete description of all the methods, properties, and DOM events associated with HTML5 media, see Safari DOM Additions Reference. All the methods, properties, and DOM events associated with HTMLMediaElement, HTMLAudioElement, and HTMLVideoElement are exposed to JavaScript.

A Simple JavaScript Media Controller and Resizer

Any of the standard ways to address an HTML element in JavaScript can be used with <audio> or <video> elements. You can assign the element a name or an id, use the tag name, or use the element’s place in the DOM hierarchy. The example in Listing 2-1 uses the tag name. The example creates a simple play/pause movie control in JavaScript, with additional controls to toggle the video size between normal and doubled. It is intended to illustrate, in the simplest possible way, addressing a media element, reading and setting properties, and calling methods.

Listing 2-1  Adding simple JavaScript controls

<!DOCTYPE html>
<html>
  <head>
    <title>Simple JavaScript Controller</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <script type="text/javascript">
 
       function playPause() {
        var myVideo = document.getElementsByTagName('video')[0];
         if (myVideo.paused)
            myVideo.play();
         else
            myVideo.pause();
       }
 
       function makeBig() {
          var myVideo = document.getElementsByTagName('video')[0];
          myVideo.height = (myVideo.videoHeight * 2 ) ;
        }
       function makeNormal() {
          var myVideo = document.getElementsByTagName('video')[0];
          myVideo.height = (myVideo.videoHeight) ;
        }
 
    </script>
  </head>
 
  <body>
     <div class="video-player" align="center">
      <video src="myMovie.m4v" poster="poster.jpg" ></video>
      <br>
      <a href="javascript:playPause();">Play/Pause</a> <br>
      <a href="javascript:makeBig();">2x Size</a> |
      <a href="javascript:makeNormal();">1x Size</a> <br>
     </div>
  </body>
</html>

The previous example gets two read-only properties: paused and videoHeight (the native height of the video). It calls two methods: play() and pause(). And it sets one read/write property: height. Recall that setting only the height or width causes the video to scale up or down while retaining its native aspect ratio.

Note: Safari for iPhone OS version 3.2 does not support dynamically resizing video on the iPad.

Using DOM Events to Monitor Load Progress

One of the common tasks for a movie controller is to display a progress indicator showing how much of the movie has loaded so far. One way to do this is to constantly poll the media element’s buffered property, to see how much of the movie has buffered, but this is a waste of time and energy. It wastes processor time and often battery charge, and it slows the loading process.

A much better approach is to create an event listener that is notified when the browser has something to report. Once the movie has begun to load, you can listen for progress events. You can read the buffered value when the browser reports progress and display it as a percentage of the movie’s duration.

Another useful DOM event is canplaythrough, a logical point to begin playing programmatically.

Listing 2-2 loads a large movie from a remote server so you can see the progress changing. It installs an event listener for progress events and another for the canplaythrough event. It indicates the percentage loaded by changing the inner HTML of a paragraph element.

You can copy and paste the example into a text editor and save it as HTML to see it in action.

Listing 2-2  Installing DOM event listeners

<!DOCTYPE html>
<html>
 <head>
    <title>JavaScript Progress Monitor</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <script type="text/javascript">
 
 
       function getPercentProg() {
         var myVideo = document.getElementsByTagName('video')[0];
         var soFar = parseInt(((myVideo.buffered.end(0) / myVideo.duration) * 100));
         document.getElementById("loadStatus").innerHTML =  soFar + '%';
        }
 
       function myAutoPlay() {
          var myVideo = document.getElementsByTagName('video')[0];
          myVideo.play();
        }
 
        function addMyListeners(){
         var myVideo = document.getElementsByTagName('video')[0];
         myVideo.addEventListener('progress',getPercentProg,false);
         myVideo.addEventListener('canplaythrough',myAutoPlay,false);
        }
 
   </script>
  </head>
 
  <body onLoad="addMyListeners()">
     <div align=center>
        <video controls
        src="http://homepage.mac.com/qt4web/sunrisemeditations/myMovie.m4v" >
        </video>
        <p ID="loadStatus">
         MOVIE LOADING...
        </p>
      </div>
  </body>
</html>

Note: On the iPad, Safari does not begin downloading until the user clicks the poster or placeholder. Currently, downloads begun in this manner do not emit progress events.

The buffered property is a TimeRanges object, essentially an array of start and stop times, not a single value. Consider what happens if the person watching the media uses the time scrubber to jump forward to a point in the movie that hasn’t loaded yet—the movie stops loading and jumps forward to the new point in time, then starts buffering again from there. So the buffered property can contain an array of discontinuous ranges. The example simply seeks to the end of the array and reads the last value, so it actually shows the percentage into the movie duration for which there is data. To determine precisely what percentage of a movie has loaded, taking possible discontinuities into account, iterate through the array, summing the seekable ranges, as illustrated in Listing 2-3

Listing 2-3  Summing a TimeRanges object

 var myBuffered = myVideo.buffered;
 var total = 0;
 for (ndx = 0; ndx < myBuffered.length; ndx++)
    total += (seekable.end(ndx) - seekable.start(ndx));

The buffered, played, and seekable properties are all TimeRanges objects.

Replacing a Media Source Sequentially

Another common task for a website programmer is to create a playlist of audio or video—to put together a radio set or to follow an advertisement with a program, for example. To do this, you can provide a function that listens for the ended event. The ended event is triggered only when the media ends (plays to its complete duration), not if it is paused.

When your listener function is triggered, it should change the media’s src property, then call the load method to load the new media and the play method to play it, as shown in Listing 2-4.

Listing 2-4  Replacing media sequentially

<!DOCTYPE html>
<html>
  <head>
   <title>Sequential Movies</title>
   <meta http-equiv="content-type" content="text/html; charset=utf-8">
   <script type="text/javascript">
 
       // listener function changes src
       function myNewSrc() {
          var myVideo = document.getElementsByTagName('video')[0];
          myVideo.src="http://homepage.mac.com/qt4web/sunrisemeditations/myMovie.m4v";
          myVideo.load();
          myVideo.play();
        }
       // function adds listener function to ended event -->
        function myAddListener(){
         var myVideo = document.getElementsByTagName('video')[0];
         myVideo.addEventListener('ended',myNewSrc,false);
        }
   </script>
  </head>
  <body onload="myAddListener()">
      <video controls
        src="http://homepage.mac.com/qt4web/sunrisemeditations/A-chord.m4v"
       >
      </video>
  </body>
</html>

The previous example works on both Safari for the desktop and Safari for iPhone OS, as the load() and play() methods are enabled even on cellular networks once the user has started playing the first media element.

Using JavaScript to Provide Fallback Content

It’s easy to provide fallback content for browsers that don’t support the <audio> or <video> tag using HTML (see “Fallback”). But if the browser understands the tag and can’t play any of the media you’ve specified, you need JavaScript to detect this and provide fallback content.

To do this, you need to iterate through your source types using the canPlayType method.

Important: The HTML5 specification has changed. Browsers conforming to the earlier version of the specification, including Safari 4.0.4 and earlier, return "no‚Äù if they cannot play the media type. Browsers conforming to the newer version return an empty string (‚Äú‚Äù)instead. You need to check for either response, or else check for a positive response rather than a negative one.

If the method returns “no” or the empty string (“”) for all the source types, the browser knows it can’t play any of the media, and you need to supply fallback content. If the method returns “maybe” or “probably” for any of the types, it will attempt to play the media and no fallback should be needed.

The following example creates an array of types, one for each source, and iterates through them to see if the browser thinks it can play any of them. If it exhausts the array without a positive response, none of the media types are supported, and it replaces the video element using innerHTML. Listing 2-5 displays a text message as fallback content. You could fall back to a plug-in or redirect to another page instead.

Listing 2-5  Testing for playability using JavaScript

<!DOCTYPE html>
<html>
 <head>
   <title>JavaScript Fallback</title>
   <meta http-equiv="content-type" content="text/html; charset=utf-8">
   <script type="text/javascript">
 
     function checkPlaylist() {
       var playAny = 0;
       myTypes = new Array ("video/mp4","video/ogg","video/divx");
       var nonePlayable = "Your browser cannot play these movie types."
       var myVideo = document.getElementsByTagName('video')[0];
       for (x = 0; x < myTypes.length; x++)
       { var canPlay = myVideo.canPlayType(myTypes[x]);
         if ((canPlay=="maybe") || (canPlay=="probably"))
            playAny = 1;
       }
       if (playAny==0)
         document.getElementById("video-player").innerHTML = nonePlayable;
     }
 
   </script>
 </head>
 <body onload="checkPlaylist()" >
   <div id="video-player" align=center>
     <video controls height="200" width="400">
     <source src="myMovie.m4v" type="video/mp4">
     <source src="myMovie.oga" type="video/ogg">
     <source src="myMovie.dvx" type="video/divx">
     </video>
   </div>
  </body>
</html>

Handling Playback Failure

Even if a source type is playable, that’s no guarantee that the source file is playable—the file may be missing, corrupted, misspelled, or the type attribute supplied may be incorrect. If Safari 4.0.4 or earlier attempts to play a source and cannot, it emits an error event. However, it still continues to iterate through the playable sources, so the error event may indicate only a momentary setback, not a complete failure. It’s important to check which source has failed to play.

Important: If you install an event listener for the error event, it should not trigger fallback behavior unless the currentSrc property contains the last playable source filename.

Changes in the HTML5 specification now require the media element to emit an error only if the last playable source fails, so this test should not be necessary in the future, except for compatibility with current browsers.

TThe example in Listing 2-5 iterates through the source types to see if any are playable. It saves the filename of the last playable source. If there are no playable types, it triggers a fallback. If there are playable types, it installs an error event listener. The event listener checks to see if the current source contains the last playable filename before triggering a failure fallback. (The currentSrc property includes the full path, so test for inclusion, not equality.)

Notice that when adding a listener for the error event you need to set the capture property to true, whereas for most events you set it to false.

Listing 2-6  Testing for failure using JavaScript

<!DOCTYPE html>
<html>
 <head>
   <title>JavaScript Fallback</title>
   <meta http-equiv="content-type" content="text/html; charset=utf-8">
   <script type="text/javascript">
 
     var lastPlayable;
     myTypes = new Array ("video/mp4","video/ogg","video/divx");
     mySrc = new Array ("myMovie.mp4","myMovie.oga","myMovie.dvx");
 
     function errorFallback() {
        var errorLast = "An error occurred playing " ;
        var myVideo = document.getElementsByTagName('video')[0];
        if (myVideo.currentSrc.match(lastPlayable))
         { errorLast = errorLast + lastPlayable ;
           document.getElementById("video-player").innerHTML = errorLast;
         }
      }
 
     function checkPlaylist() {
       var noPlayableTypes = "Your browser cannot play these movie types";
       var myVideo = document.getElementsByTagName('video')[0];
       var playAny = 0;
       for (x = 0; x < myTypes.length; x++)
       {
         var canPlay = myVideo.canPlayType(myTypes[x]);
         if ((canPlay=="maybe") || (canPlay=="probably"))
            { playAny = 1;
              lastPlayable=mySrc[x];
            }
        }
       if (playAny==0)
       {
       document.getElementById("video-player").innerHTML = noPlayableTypes;
       } else {
       myVideo.addEventListener('error',errorFallback,true);
       }
      }
 
   </script>
 </head>
 <body onload="checkPlaylist()" >
   <div id="video-player" align=center>
     <video controls >
     <source src="myMovie.mp4" type="video/mp4">
     <source src="myMovie.oga" type="video/ogg">
     <source src="myMovie.dvx" type="video/divx
     </video>
   </div>
  </body>
</html>

Resizing Movies to Native Size

If you know the dimensions of your movie in advance, you should specify them. Specifying the dimensions is especially important for delivering the best user experience on iPad. But you may not know the dimensions when writing the webpage. For example, your source movies may not be the same size, or sequential movies may have different dimensions. If you install a listener function for the loadedmetadatata event, you can resize the video player to the native movie size dynamically using JavaScript, as soon as the native size is known. If sequential movies may be different sizes, you can do this any time you change the source. Listing 2-7 shows how.

Listing 2-7  Resizing movies programmatically

<!DOCTYPE html>
<html>
  <head>
   <title>Resizing Movies</title>
   <meta http-equiv="content-type" content="text/html; charset=utf-8">
   <script type="text/javascript">
 
       // set height and width to native values
       function naturalSize() {
          var myVideo = document.getElementsByTagName('video')[0];
          myVideo.height=myVideo.videoHeight;
          myVideo.width=myVideo.videoWidth;
        }
       // install listener function on metadata load
        function myAddListener(){
         var myVideo = document.getElementsByTagName('video')[0];
         myVideo.addEventListener('loadedmetadata',naturalSize,false);
        }
   </script>
  </head>
  <body onload="myAddListener()" >
 
      <video src="http://homepage.mac.com/qt4web/myMovie.m4v"
             controls >
      </video>
 
  </body>
</html>



Last updated: 2010-03-18

Did this document help you? Yes It's good, but... Not helpful...