Source: util/content_database.js

/**
 * @license
 * Copyright 2015 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

goog.provide('shaka.util.ContentDatabase');

goog.require('shaka.asserts');
goog.require('shaka.util.FakeEventTarget');
goog.require('shaka.util.PublicPromise');
goog.require('shaka.util.TypedBind');



/**
 * Creates a new ContentDatabase, which manages a database for reading and
 * writing streams to and from persistent storage.
 *
 * @param {string} mode The I/O mode, which must be either 'readonly' or
 *     'readwrite'.
 * @param {shaka.util.FakeEventTarget} parent
 *
 * @constructor
 * @struct
 * @extends {shaka.util.FakeEventTarget}
 */
shaka.util.ContentDatabase = function(mode, parent) {
  shaka.asserts.assert(mode == 'readonly' || mode == 'readwrite');

  shaka.util.FakeEventTarget.call(this, parent);

  /** @private {IDBDatabase} */
  this.db_ = null;

  /** @private {string} */
  this.mode_ = mode;
};
goog.inherits(shaka.util.ContentDatabase, shaka.util.FakeEventTarget);


/**
 * The name of the IndexedDb instance.
 *
 * @const {string}
 */
shaka.util.ContentDatabase.DB_NAME = 'content_database';


/**
 * The current version of the database.
 *
 * @private {number}
 * @const
 */
shaka.util.ContentDatabase.DB_VERSION_ = 1;


/**
 * The name of the group store in the IndexedDb instance.
 *
 * @private {string}
 * @const
 */
shaka.util.ContentDatabase.GROUP_STORE_ = 'group_store';


/**
 * The name of the index store in the IndexedDb instance.
 *
 * @private {string}
 * @const
 */
shaka.util.ContentDatabase.INDEX_STORE_ = 'stream_index_store';


/**
 * The name of the content store in the IndexedDb instance.
 *
 * @private {string}
 * @const
 */
shaka.util.ContentDatabase.CONTENT_STORE_ = 'content_store';


/**
 * @typedef {{
 *    group_id: number,
 *    stream_ids: !Array.<number>,
 *    session_ids: !Array.<string>,
 *    duration: ?number,
 *    key_system: string,
 *    license_server: string,
 *    with_credentials: boolean,
 *    distinctive_identifier: boolean,
 *    audio_robustness: string,
 *    video_robustness: string
 *  }}
 */
shaka.util.ContentDatabase.GroupInformation;


/**
 * @typedef {{
 *    stream_id: number,
 *    mime_type: string,
 *    codecs: string,
 *    init_segment: ArrayBuffer,
 *    references: !Array.<shaka.util.ContentDatabase.SegmentInformation>
 *  }}
 */
shaka.util.ContentDatabase.StreamIndex;


/**
 * @typedef {{
 *    start_time: number,
 *    end_time: ?number,
 *    start_byte: number,
 *    url: string
 *  }}
 */
shaka.util.ContentDatabase.SegmentInformation;


/**
 * Opens a connection to the database and sets up the database if required. If
 * a new version number is given the onupgradeneeded event will be fired. Must
 * be run before any operations can be performed on the database.
 * The database will have the structure:
 * Group Store: {
 *    group_id: number
 *    stream_ids: Array.<number>
 *    session_ids: Array.<string>
 * }
 * Index Store: {
 *    stream_id: number,
 *    mime_type: string,
 *    references: [{shaka.util.ContentDatabase.SegmentInformation}]
 * }
 * Content Store: {
 *    stream_id: number,
 *    segment_id: number,
 *    content: ArrayBuffer
 * }
 * @return {!Promise}
 */
shaka.util.ContentDatabase.prototype.setUpDatabase = function() {
  if (!window.indexedDB) {
    var error = new Error('Persistant storage requires IndexedDB support.');
    error.type = 'storage';
    return Promise.reject(error);
  }

  if (this.db_) {
    var error = new Error('A database connection is already open.');
    error.type = 'storage';
    return Promise.reject(error);
  }

  var p = new shaka.util.PublicPromise();
  var indexedDB = window.indexedDB;
  var request = indexedDB.open(shaka.util.ContentDatabase.DB_NAME,
                               shaka.util.ContentDatabase.DB_VERSION_);

  request.onupgradeneeded = shaka.util.TypedBind(this,
      /** @param {!Event} e */
      function(e) {
        this.db_ = e.target.result;

        this.createStore_(
            shaka.util.ContentDatabase.GROUP_STORE_, {keyPath: 'group_id'});
        this.createStore_(
            shaka.util.ContentDatabase.INDEX_STORE_, {keyPath: 'stream_id'});
        var contentStore = this.createStore_(
            shaka.util.ContentDatabase.CONTENT_STORE_, {autoIncrement: 'true'});

        contentStore.createIndex('segment',
                                 ['stream_id', 'segment_id'],
                                 {unique: true});
        contentStore.createIndex('stream',
                                 'stream_id',
                                 {unique: false});
      });

  request.onsuccess = shaka.util.TypedBind(this,
      /** @param {!Event} e */
      function(e) {
        this.db_ = e.target.result;
        p.resolve();
      });

  request.onerror = function(e) { p.reject(request.error); };
  return p;
};


/**
 * Closes the connection to the database.
 */
shaka.util.ContentDatabase.prototype.closeDatabaseConnection = function() {
  if (this.db_) {
    this.db_.close();
    this.db_ = null;
  }
};


/**
 * Closes the connection to the database if required and then deletes the
 * database. The database can only be deleted if there are no other connections
 * to the database.
 * @return {!Promise}
 */
shaka.util.ContentDatabase.prototype.deleteDatabase = function() {
  var p = new shaka.util.PublicPromise();
  this.closeDatabaseConnection();
  var deleteRequest = window.indexedDB.deleteDatabase(
      shaka.util.ContentDatabase.DB_NAME);

  deleteRequest.onsuccess = function(e) {
    shaka.asserts.assert(e.newVersion == null);
    p.resolve();
  };
  deleteRequest.onerror = function(e) { p.reject(deleteRequest.error); };
  return p;
};


/**
 * Creates an object store in the database. It will replace any previous object
 * store with the same name in this database.
 * @param {string} name The unique name of the object store.
 * @param {Object} options The options for this object store including
 *    keyPath and autoIncrement, other options will be ignored.
 * @return {!IDBObjectStore}
 * @private
 */
shaka.util.ContentDatabase.prototype.createStore_ = function(name, options) {
  if (this.db_.objectStoreNames.contains(name)) {
    this.db_.deleteObjectStore(name);
  }
  return this.db_.createObjectStore(name, options);
};


/**
 * Opens a reference to the content store.
 * @return {!IDBObjectStore} A reference to the content store.
 * @protected
 */
shaka.util.ContentDatabase.prototype.getContentStore = function() {
  return this.getStore_(shaka.util.ContentDatabase.CONTENT_STORE_);
};


/**
 * Opens a reference to the index store.
 * @return {!IDBObjectStore} A reference to the index store.
 * @protected
 */
shaka.util.ContentDatabase.prototype.getIndexStore = function() {
  return this.getStore_(shaka.util.ContentDatabase.INDEX_STORE_);
};


/**
 * Opens a reference to the group store.
 * @return {!IDBObjectStore} A reference to the group store.
 * @protected
 */
shaka.util.ContentDatabase.prototype.getGroupStore = function() {
  return this.getStore_(shaka.util.ContentDatabase.GROUP_STORE_);
};


/**
 * Opens a reference to a store.
 * @param {string} storeName The name of a store in the database.
 * @return {!IDBObjectStore} A reference to a store.
 * @private
 */
shaka.util.ContentDatabase.prototype.getStore_ = function(storeName) {
  shaka.asserts.assert(this.db_, 'A database connection should be open.');
  var trans = this.db_.transaction([storeName], this.mode_);
  return trans.objectStore(storeName);
};


/**
 * Retrieves an item from a store in the database.
 * @param {!IDBObjectStore|!IDBIndex} store The store to request an item from.
 * @param {number|!Array} id The unique id(s) of item in the store.
 * @return {!Promise}
 * @protected
 */
shaka.util.ContentDatabase.prototype.retrieveItem = function(
    store, id) {
  var p = new shaka.util.PublicPromise();

  var request = store.get(id);

  request.onerror = function(e) { p.reject(request.error); };
  request.onsuccess = function() {
    if (request.result) {
      p.resolve(request.result);
    } else {
      var error = new Error('Item not found.');
      error.type = 'storage';
      p.reject(error);
    }
  };

  return p;
};