HTML5 Off Line: Storing and retrieving Videos with IndexedDB

There’s some disagreement among developers as to the importance of an offline experience for HTML5 apps. Some people say it’s not that important  because everything is always connected. That may be true if you live in the heart of Silicone Valley but if you live in the hills of New England, like I do, or in Central or South America (where the first Firefox OS devices will ship), then that may not be true.

Personally, I think a great app needs to include an offline strategy. “Adaptive” design shouldn’t be only about the user interface.

You’ve probably heard about HTML AppCache, but AppCache is only designed for certain limited use cases.

Think of media files for example. AppCache is declarative, meaning that you specify the resources that should be downloaded and cached in the cache manifest. AppCache is also non deterministic so it downloads the referenced resources on a best efforts basis, primarily for use “next time”.

In the case of a content driven app, this doesn’t solve all the problems. Take, for example an app for a video podcast. There may be hundreds, or even thousands of video files. The user wouldn’t want the run-time to just start downloading thousands of videos, many of which they may never even watch. The user wants to watch only certain videos and only those certain videos should be downloaded..

Since I pay for band width (both as an application hoster and as an Internet user) I don’t want a preferred video to be download each time a user wants to view it.

HTML5 local storage is not designed for lots of data or for large objects.

Gecko, the engine behind Mozilla Apps and Firefox OS Apps supports an embedded database – IndexedDB.

IndexedDB has been available in Firefox for some time and is mostly implemented in Google Chrome. It will also be implemented in Microsoft’s IE 10.  Safari, Opera, etc have implemented WebSQL which the the W3C has stopped working on.

(The demo that follows did not work in Chroime at the time of this writing, probably related to this issue. Since the focus if this article revolves around Firefox OS and Mozilla Apps I’m not going to try to debug the issue on Chrome at this time. Please email me if you get it to work in Chrome.)

I have to admit that I found IndexedDB a bit daunting when I first started with it. It’s different than the SQL databases that I have worked with because it does not involve user created SQL syntax in order to interact with the database. Instead, it leverages what feels to me more like an  ORM (Object Relation Mapping) approach. If you’re not used to ORM syntax there is a bit of a learning curve but once you adjust to it I think you’ll find that your code reads and gets written in a more fluid manner.

So given the use case described above for a video viewing app, here is the English pseudo code for the app’s function.

  1. Fetch a video from the Internet and download it into the browser DOM.
  2. Store that video in an IndexedDB object store
  3. In response to some user generated event …
  4. Retrieve the previously stored video from the IndexedDB object store.
  5. Play the retrieved Video

The demo starts with an index.html page

Note the  link to the put.html page.

Put.html will automatically start downloading a video file.

See the “WORKING” message ?

When the file has been downloaded and saved that message will change to indicate that you can click the link to move on to the display page.

Here is the put.html markup.


<!DOCTYPE html>
<html lang="en">
   <head>
      <meta charset="UTF-8">
      <title>HTML5 - Storing a video file IN IndexedDB</title>
      <link rel="stylesheet" href="css/base.css" type="text/css" 
            media="screen">
   </head>
   <body>
      <div class="container">
         <header class="header" role="banner">
            <h1>Storing a video file IN IndexedDB</h1>
         </header>
         <div class="main">
            <article class="main-content" role="main">
               <span id="fetchstatus">
                  WORKING : Video File is Downloading
               </span>
               <br /><br />
               <a href="show.html">
                  <strong>
                     Click to DISPLAY a video retrieved FROM IndexedDB
                  </strong>
               </a>
            </article>
         </div>
      </div>
      <script src="js/put.js"></script>
   </body>
</html>


And the included JavaScript file – put.js

I think the code’s functionality is pretty straight forward.


(function () {

   window.indexedDB = window.indexedDB || window.webkitIndexedDB ||
                      window.mozIndexedDB || window.OIndexedDB || 
                      window.msIndexedDB;
                       
   var IDBTransaction = window.IDBTransaction || 
                        window.webkitIDBTransaction || 
                        window.OIDBTransaction || 
                        window.msIDBTransaction;
                         
   var dbVersion = 1.0; 
   var indexedDB = window.indexedDB;
   var dlStatusText = document.getElementById("fetchstatus");

   // Create/open database
   var request = indexedDB.open("VideoFiles", dbVersion),
      db,
      createObjectStore = function (dataBase) {
          dataBase.createObjectStore("Videos");
      },

      getVideoFile = function () {
         var xhr = new XMLHttpRequest(),
         blob;
         // Get the Video file from the server.
         xhr.open("GET", "Heilman-Betts.webm", true);     
         xhr.responseType = "blob";
         xhr.addEventListener("load", function () {
            if (xhr.status === 200) {
                blob = xhr.response;
                putVideoInDb(blob);
                dlStatusText.innerHTML = "SUCCESS: Video file downloaded.";
            }
            else {
                dlStatusText.innerHTML = "ERROR: Unable to download video.";
            }
          }, false);
          xhr.send();
      },

      putVideoInDb = function (blob) {
         var transaction = db.transaction(["Videos"], "readwrite");
         var put = transaction.objectStore("Videos").put(blob, "savedvideo");
      };


    request.onerror = function (event) {
        console.log("Error creating/accessing IndexedDB database");
    };

    request.onsuccess = function (event) {
        console.log("Success creating/accessing IndexedDB database");
        db = request.result;

        db.onerror = function (event) {
            console.log("Error creating/accessing IndexedDB database");
        };
        
        getVideoFile();
       
    }
    
    // For future use. Currently only in latest Firefox versions
    request.onupgradeneeded = function (event) {
        createObjectStore(event.target.result);
    };
    
})();


Note both the xhr event listener and the onsuccess / onerror methods for IndexedDB funtions which indicate that both APIs are asynchronous.

Now that the video file is stored in the IndexedDB store, we can click on the lick to navigate to the show.html page which will automatically retrieve the video from the IndexedDB store and ready it for playing on the page.

Here is the show.html markup.


<!DOCTYPE html>
<html lang="en">
   <head>
      <meta charset="UTF-8">
      <title>HTML5 - Storing and Playing Videos with  IndexedDB</title>
      <link rel="stylesheet" href="css/base.css" type="text/css" 
            media="screen">
   </head>
   <body>
      <div class="container">
         <header class="header" role="banner">
           <h1>Display a video stored in IndexedDB</h1>
         </header>
         <div class="main">
            <video  id="Video" type="video/webm" controls>
               <p>
               If you are reading this,
               it is because your browser does not support the 
               HTML5 video element.
               </p>
            </video>
            <p>
                Video MIME type is : <span id="vidMimeDisplay"></span>
            </p>
         </div>
      </div>
      <script src="js/show.js"></script>
   </body>
</html>

And the show.js code…..


(function () {
    // IndexedDB
    window.indexedDB = window.indexedDB || window.webkitIndexedDB || 
                       window.mozIndexedDB || window.OIndexedDB || 
                       window.msIndexedDB,
    IDBTransaction = window.IDBTransaction || 
                     window.webkitIDBTransaction ||
                     window.OIDBTransaction || window.msIDBTransaction,
    dbVersion = 1.0;

    var indexedDB = window.indexedDB;

    // Create/open database
    var request = indexedDB.open("VideoFiles");
    
    request.onerror = function (event) {
        // Failed to Open the indexedDB database
    };

    request.onsuccess = function (event) {
		db = request.result;
		
		// Open a transaction to the database
        var transaction = db.transaction(["Videos"], "readwrite");

        // Retrieve the video file
        transaction.objectStore("Videos").get("savedvideo").onsuccess = 
        function (event) {
				
        var videoFile = event.target.result;
        var URL = window.URL || window.webkitURL;
        var videoURL = URL.createObjectURL(videoFile);

        // Set video src to ObjectURL
        var videoElement = document.getElementById("Video");
            videoElement.setAttribute("src", videoURL);

       var mimeDisplayElement = document.getElementById("vidMimeDisplay");
           mimeDisplayElement.innerHTML = videoFile.type;
	    };
    }
})();

Which results in…..

Now the video can be watched and re-watched without having to re-download the video for each viewing.

If we configured the player for app caching the videos could even be played with no internet connection. (I’ll do an example of that if a future post.)


[ CLICK HERE ] To download the code.