Offline Playback

Shaka Player supports offline playback since v1.3.0. Offline streams are handled by {@link 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 {@tutorial 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 {@link shaka.player.OfflineVideoSource} to be loaded into the {@link 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 {@link 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

{@link shaka.player.OfflineVideoSource} can only store encrypted streams on platforms that support persistent licenses.

Offline playback relies on the {@link http://goo.gl/TFHaXL Indexed Database API} for storage of content. Indexed Database is currently supported by most web browsers.