Tutorial: Offline Playback with the Shaka Player

Offline Playback

Shaka Player supports offline playback since v1.3.0. Offline streams are handled by shaka.player.OfflineVideoSource. The offline API works similarily to the other video source APIs, but adds the functionality to store a group, retrieve a list of all stored groups and to delete a group from storage.

A group refers to one video stream and one audio stream, if present, from a DASH MPD. Storage of text content is not yet supported. Each group will have a unique ID, which should be persisted by the application for later playback.

Storing Content

If you have not familiarized yourself with the the Player, please start with DASH Playback with the Shaka Player. In order to store content you need to install the polyfills and initialize the player, but do not need to load the video source.

Here is a simple page which demonstrates basic offline storage:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>TurtleTube - Offline</title>
    <!-- Load the Shaka Player library. -->
    <script src="shaka-player.compiled.js"></script>
  </head>
  <body>
    <video id="video"
           width="640" height="480"
           crossorigin="anonymous"
           controls>
      Your browser does not support HTML5 video.
    </video>
  </body>
  <script>
    var video;

    function initPlayer() {
      // Install polyfills.
      shaka.polyfill.installAll();

      // Find the video element.
      video = document.getElementById('video');

      // Attach the player to the window so that it can be easily debugged.
      window.player = new shaka.player.Player(video);

      // Listen for errors from the Player.
      player.addEventListener('error', function(event) {
        console.error(event);
      });
    }

    function initialize() {
      if (!window.player)
        initPlayer();
    }

    function chooseTracks(videoSource) {
      var ids = [];

      var videoTracks = videoSource.getVideoTracks();
      if (videoTracks.length) {
        videoTracks.sort(shaka.player.VideoTrack.compare);
        // Choosing the smallest track.
        var track = videoTracks[0];
        ids.push(track.id);
      }

      var audioTracks = videoSource.getAudioTracks();
      if (audioTracks.length) {
        // The video source gives you the preferred language first.
        // Remove any tracks from other languages first.
        var lang = audioTracks[0].lang;
        audioTracks = audioTracks.filter(function(track) {
          return track.lang == lang;
        });
        // From what's left, choose the middle stream.  If we have high, medium,
        // and low quality audio, this is medium.  If we only have high and low,
        // this is high.
        var index = Math.floor(audioTracks.length / 2);
        ids.push(audioTracks[index].id);
      }

      // Return IDs of chosen tracks.
      return Promise.resolve(ids);
    }

    function storeContent() {
      // Construct an OfflineVideoSource.
      var offlineSource = new shaka.player.OfflineVideoSource(
          null, // groupId, not used when storing content.
          null); // estimator, optional parameter.

      // Listen for progress events from the OfflineVideoSource.
      offlineSource.addEventListener('progress', function(event) {
        // Percentage complete is the detail field of the event.
        console.log(
            'Content storage is ' + event.detail.toFixed(2) + '% complete.');
      });

      // Store content from MPD url.
      var mpdUrl = 'https://turtle-tube.appspot.com/t/t2/dash.mpd';
      var preferredLanguage = 'en-US';
      return offlineSource.store(
          mpdUrl,
          preferredLanguage,
          null, // interpretContentProtection, not needed for clear content.
          chooseTracks.bind(null, offlineSource)
      ).then(
          function(groupId) {
            window.groupId = groupId;
            console.log('Stored content under group ID ' + groupId);
          }
      );
    }

    function test() {
      initialize();
      storeContent().catch(
          function(e) {
            console.error(e);
          });
    }

    document.addEventListener('DOMContentLoaded', test);
  </script>
</html>

Playing Stored Content

In order to playback content, you need the group ID of the stored content. The group ID will be used to create a new shaka.player.OfflineVideoSource to be loaded into the shaka.player.Player.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>TurtleTube - Offline</title>
    <!-- Load the Shaka Player library. -->
    <script src="shaka-player.compiled.js"></script>
  </head>
  <body>
    <video id="video"
           width="640" height="480"
           crossorigin="anonymous"
           controls>
      Your browser does not support HTML5 video.
    </video>
  </body>
  <script>
    var video;

    function initPlayer() {
      // Install polyfills.
      shaka.polyfill.installAll();

      // Find the video element.
      video = document.getElementById('video');

      // Attach the player to the window so that it can be easily debugged.
      window.player = new shaka.player.Player(video);

      // Listen for errors from the Player.
      player.addEventListener('error', function(event) {
        console.error(event);
      });
    }

    function initialize() {
      if (!window.player)
        initPlayer();
    }

    function chooseTracks(videoSource) {
      var ids = [];

      var videoTracks = videoSource.getVideoTracks();
      if (videoTracks.length) {
        videoTracks.sort(shaka.player.VideoTrack.compare);
        // Choosing the smallest track.
        var track = videoTracks[0];
        ids.push(track.id);
      }

      var audioTracks = videoSource.getAudioTracks();
      if (audioTracks.length) {
        // The video source gives you the preferred language first.
        // Remove any tracks from other languages first.
        var lang = audioTracks[0].lang;
        audioTracks = audioTracks.filter(function(track) {
          return track.lang == lang;
        });
        // From what's left, choose the middle stream.  If we have high, medium,
        // and low quality audio, this is medium.  If we only have high and low,
        // this is high.
        var index = Math.floor(audioTracks.length / 2);
        ids.push(audioTracks[index].id);
      }

      // Return IDs of chosen tracks.
      return Promise.resolve(ids);
    }

    function storeContent() {
      // Construct an OfflineVideoSource.
      var offlineSource = new shaka.player.OfflineVideoSource(
          null, // groupId, not used when storing content.
          null); // estimator, optional parameter.

      // Listen for progress events from the OfflineVideoSource.
      offlineSource.addEventListener('progress', function(event) {
        // Percentage complete is the detail field of the event.
        console.log(
            'Content storage is ' + event.detail.toFixed(2) + '% complete.');
      });

      // Store content from MPD url.
      var mpdUrl = 'https://turtle-tube.appspot.com/t/t2/dash.mpd';
      var preferredLanguage = 'en-US';
      return offlineSource.store(
          mpdUrl,
          preferredLanguage,
          null, // interpretContentProtection, not needed for clear content.
          chooseTracks.bind(null, offlineSource)
      ).then(
          function(groupId) {
            window.groupId = groupId;
            console.log('Stored content under group ID ' + groupId);
          }
      );
    }

    function playOfflineContent() {
      // Construct an OfflineVideoSource and load with player.
      var offlineSource =
          new shaka.player.OfflineVideoSource(window.groupId, null);
      return window.player.load(offlineSource).then(
          function() {
            video.play();
            console.log('Offline content with group ID ' + window.groupId +
                  ' ready for playback.');
          });
    }

    function test() {
      initialize();
      storeContent().then(playOfflineContent).catch(
          function(e) {
            console.error(e);
          });
    }

    document.addEventListener('DOMContentLoaded', test);
  </script>
</html>

Deleting Stored Content

Similar to playing stored content, you will create a new shaka.player.OfflineVideoSource with a known group ID to delete content.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>TurtleTube - Offline</title>
    <!-- Load the Shaka Player library. -->
    <script src="shaka-player.compiled.js"></script>
  </head>
  <body>
    <video id="video"
           width="640" height="480"
           crossorigin="anonymous"
           controls>
      Your browser does not support HTML5 video.
    </video>
  </body>
  <script>
    var video;

    function initPlayer() {
      // Install polyfills.
      shaka.polyfill.installAll();

      // Find the video element.
      video = document.getElementById('video');

      // Attach the player to the window so that it can be easily debugged.
      window.player = new shaka.player.Player(video);

      // Listen for errors from the Player.
      player.addEventListener('error', function(event) {
        console.error(event);
      });
    }

    function initialize() {
      if (!window.player)
        initPlayer();
    }

    function chooseTracks(videoSource) {
      var ids = [];

      var videoTracks = videoSource.getVideoTracks();
      if (videoTracks.length) {
        videoTracks.sort(shaka.player.VideoTrack.compare);
        // Choosing the smallest track.
        var track = videoTracks[0];
        ids.push(track.id);
      }

      var audioTracks = videoSource.getAudioTracks();
      if (audioTracks.length) {
        // The video source gives you the preferred language first.
        // Remove any tracks from other languages first.
        var lang = audioTracks[0].lang;
        audioTracks = audioTracks.filter(function(track) {
          return track.lang == lang;
        });
        // From what's left, choose the middle stream.  If we have high, medium,
        // and low quality audio, this is medium.  If we only have high and low,
        // this is high.
        var index = Math.floor(audioTracks.length / 2);
        ids.push(audioTracks[index].id);
      }

      // Return IDs of chosen tracks.
      return Promise.resolve(ids);
    }

    function storeContent() {
      // Construct an OfflineVideoSource.
      var offlineSource = new shaka.player.OfflineVideoSource(
          null, // groupId, not used when storing content.
          null); // estimator, optional parameter.

      // Listen for progress events from the OfflineVideoSource.
      offlineSource.addEventListener('progress', function(event) {
        // Percentage complete is the detail field of the event.
        console.log(
            'Content storage is ' + event.detail.toFixed(2) + '% complete.');
      });

      // Store content from MPD url.
      var mpdUrl = 'https://turtle-tube.appspot.com/t/t2/dash.mpd';
      var preferredLanguage = 'en-US';
      return offlineSource.store(
          mpdUrl,
          preferredLanguage,
          null, // interpretContentProtection, not needed for clear content.
          chooseTracks.bind(null, offlineSource)
      ).then(
          function(groupId) {
            window.groupId = groupId;
            console.log('Stored content under group ID ' + groupId);
          }
      );
    }

    function playOfflineContent() {
      // Construct an OfflineVideoSource and load with player.
      var offlineSource =
          new shaka.player.OfflineVideoSource(window.groupId, null);
      return window.player.load(offlineSource).then(
          function() {
            video.play();
            console.log('Offline content with group ID ' + window.groupId +
                  ' ready for playback.');
          });
    }

    function deleteOfflineContent() {
      var offlineSource =
          new shaka.player.OfflineVideoSource(window.groupId, null);
      return offlineSource.deleteGroup().then(
          function() {
            console.log('Offline content with group ID ' + window.groupId +
                        ' successfully deleted.');
          });
    }

    function test() {
      initialize();
      storeContent().then(deleteOfflineContent).catch(
          function(e) {
            console.error(e);
          });
    }

    document.addEventListener('DOMContentLoaded', test);
  </script>
</html>

Offline Caveats

shaka.player.OfflineVideoSource can only store encrypted streams on platforms that support persistent licenses.

Offline playback relies on the Indexed Database API for storage of content. Indexed Database is currently supported by most web browsers.