/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.offline.StorageCellHandle');
goog.provide('shaka.offline.StorageCellPath');
goog.provide('shaka.offline.StorageMuxer');
goog.require('shaka.log');
goog.require('shaka.util.Error');
goog.require('shaka.util.IDestroyable');
/**
* @typedef {{
* mechanism: string,
* cell: string
* }}
*
* @property {string} mechanism
* The name of the mechanism that holds the cell.
* @property {string} cell
* The name of the cell in the mechanism.
*/
shaka.offline.StorageCellPath;
/**
* @typedef {{
* path: shaka.offline.StorageCellPath,
* cell: !shaka.extern.StorageCell
* }}
*
* @property {shaka.offline.StorageCellPath} path
* The path that maps to the cell.
* @property {shaka.extern.StorageCell} cell
* The storage cell that the path points to within the storage muxer.
*/
shaka.offline.StorageCellHandle;
// TODO: revisit this when Closure Compiler supports partially-exported classes.
/**
* StorageMuxer is responsible for managing StorageMechanisms and addressing
* cells. The primary purpose of the muxer is to give the caller the correct
* cell for the operations they want to perform.
*
* |findActive| will be used when the caller wants a cell that supports
* add-operations. This will be used when saving new content to storage.
*
* |findAll| will be used when the caller want to look at all the content
* in storage.
*
* |resolvePath| will be used to convert a path (from |findActive| and
* |findAll|) into a cell, which it then returns.
*
* @implements {shaka.util.IDestroyable}
* @export
*/
shaka.offline.StorageMuxer = class {
/** */
constructor() {
/**
* A key in this map is the name given when registering a StorageMechanism.
*
* @private {!Map.<string, !shaka.extern.StorageMechanism>}
*/
this.mechanisms_ = new Map();
}
// TODO: revisit this when the compiler supports partially-exported classes.
/**
* Free all resources used by the muxer, mechanisms, and cells. This should
* not affect the stored content.
*
* @override
* @export
*/
destroy() {
/** @type {!Array.<!Promise>} */
const destroys = [];
for (const mechanism of this.mechanisms_.values()) {
destroys.push(mechanism.destroy());
}
// Empty the map so that subsequent calls will be no-ops.
this.mechanisms_.clear();
return Promise.all(destroys);
}
/**
* Initialize the storage muxer. This must be called before any other calls.
* This will initialize the muxer to use all mechanisms that have been
* registered with |StorageMuxer.register|.
*
* @return {!Promise}
*/
init() {
// Add the new instance of each mechanism to the muxer.
const registry = shaka.offline.StorageMuxer.getRegistry_();
registry.forEach((factory, name) => {
const mech = factory();
if (mech) {
this.mechanisms_.set(name, mech);
} else {
shaka.log.info(
'Skipping ' + name + ' as it is not supported on this platform');
}
});
/** @type {!Array.<!Promise>} */
const initPromises = [];
for (const mechanism of this.mechanisms_.values()) {
initPromises.push(mechanism.init());
}
return Promise.all(initPromises);
}
/**
* Get a promise that will resolve with a storage cell that supports
* add-operations. If no cell can be found, the promise will be rejected.
*
* @return {shaka.offline.StorageCellHandle}
*/
getActive() {
/** @type {?shaka.offline.StorageCellHandle} */
let handle = null;
this.mechanisms_.forEach((mechanism, mechanismName) => {
mechanism.getCells().forEach((cell, cellName) => {
// If this cell is not useful to us or we already have a handle, then
// we don't need to make a new handle.
if (cell.hasFixedKeySpace() || handle) {
return;
}
const path = {
mechanism: mechanismName,
cell: cellName,
};
handle = {
path: path,
cell: cell,
};
});
});
if (handle) {
return /** @type {shaka.offline.StorageCellHandle} */(handle);
}
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.MISSING_STORAGE_CELL,
'Could not find a cell that supports add-operations');
}
/**
* @param {function(!shaka.offline.StorageCellPath,
* !shaka.extern.StorageCell)} callback
*/
forEachCell(callback) {
this.mechanisms_.forEach((mechanism, mechanismName) => {
mechanism.getCells().forEach((cell, cellName) => {
const path = {
mechanism: mechanismName,
cell: cellName,
};
callback(path, cell);
});
});
}
/**
* Get a specific storage cell. The promise will resolve with the storage
* cell if it is found. If the storage cell is not found, the promise will
* be rejected.
*
* @param {string} mechanismName
* @param {string} cellName
* @return {!shaka.extern.StorageCell}
*/
getCell(mechanismName, cellName) {
const mechanism = this.mechanisms_.get(mechanismName);
if (!mechanism) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.MISSING_STORAGE_CELL,
'Could not find mechanism with name ' + mechanismName);
}
const cell = mechanism.getCells().get(cellName);
if (!cell) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.MISSING_STORAGE_CELL,
'Could not find cell with name ' + cellName);
}
return cell;
}
/**
* @param {function(!shaka.extern.EmeSessionStorageCell)} callback
*/
forEachEmeSessionCell(callback) {
this.mechanisms_.forEach((mechanism, name) => {
callback(mechanism.getEmeSessionCell());
});
}
/**
* Gets an arbitrary EME session cell that can be used for storing new session
* info.
*
* @return {!shaka.extern.EmeSessionStorageCell}
*/
getEmeSessionCell() {
const mechanisms = Array.from(this.mechanisms_.keys());
if (!mechanisms.length) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.STORAGE_NOT_SUPPORTED,
'No supported storage mechanisms found');
}
return this.mechanisms_.get(mechanisms[0]).getEmeSessionCell();
}
/**
* Find the cell that the path points to. A path is made up of a mount point
* and a cell id. If a cell can be found, the cell will be returned. If no
* cell is found, null will be returned.
*
* @param {shaka.offline.StorageCellPath} path
* @return {shaka.extern.StorageCell}
*/
resolvePath(path) {
const mechanism = this.mechanisms_.get(path.mechanism);
if (!mechanism) {
return null;
}
return mechanism.getCells().get(path.cell);
}
/**
* This will erase all previous content from storage. Using paths obtained
* before calling |erase| is discouraged, as cells may have changed during a
* erase.
*
* @return {!Promise}
*/
async erase() {
// If we have initialized, we will use the existing mechanism instances.
/** @type {!Array.<!shaka.extern.StorageMechanism>} */
const mechanisms = Array.from(this.mechanisms_.values());
const alreadyInitialized = mechanisms.length > 0;
// If we have not initialized, we should still be able to erase. This is
// critical to our ability to wipe the DB in case of a version mismatch.
// If there are no instances, create temporary ones and destroy them later.
if (!alreadyInitialized) {
const registry = shaka.offline.StorageMuxer.getRegistry_();
registry.forEach((factory, name) => {
const mech = factory();
if (mech) {
mechanisms.push(mech);
}
});
}
// Erase all storage mechanisms.
await Promise.all(mechanisms.map((m) => m.erase()));
// If we were erasing temporary instances, destroy them, too.
if (!alreadyInitialized) {
await Promise.all(mechanisms.map((m) => m.destroy()));
}
}
/**
* Register a storage mechanism for use with the default storage muxer. This
* will have no effect on any storage muxer already in main memory.
*
* @param {string} name
* @param {function():shaka.extern.StorageMechanism} factory
* @export
*/
static register(name, factory) {
shaka.offline.StorageMuxer.registry_.set(name, factory);
}
/**
* Unregister a storage mechanism for use with the default storage muxer. This
* will have no effect on any storage muxer already in main memory.
*
* @param {string} name The name that the storage mechanism was registered
* under.
* @export
*/
static unregister(name) {
shaka.offline.StorageMuxer.registry_.delete(name);
}
/**
* Check if there is support for storage on this platform. It is assumed that
* if there are any mechanisms registered, it means that storage is supported
* on this platform. We do not check if the mechanisms have any cells.
*
* @return {boolean}
*/
static support() {
const registry = shaka.offline.StorageMuxer.getRegistry_();
// Make sure that we will have SOME mechanisms created by creating a
// mechanism and immediately destroying it.
for (const create of registry.values()) {
const instance = create();
if (instance) {
instance.destroy();
return true;
}
}
return false;
}
/**
* Replace the mechanism map used by the muxer. This should only be used
* in testing.
*
* @param {Map.<string, function():shaka.extern.StorageMechanism>} map
*/
static overrideSupport(map) {
shaka.offline.StorageMuxer.override_ = map;
}
/**
* Undo a previous call to |overrideSupport|.
*/
static clearOverride() {
shaka.offline.StorageMuxer.override_ = null;
}
/**
* Get the registry. If the support has been disabled, this will always
* an empty registry. Reading should always be done via |getRegistry_|.
*
* @return {!Map.<string, function():shaka.extern.StorageMechanism>}
* @private
*/
static getRegistry_() {
const override = shaka.offline.StorageMuxer.override_;
const registry = shaka.offline.StorageMuxer.registry_;
if (COMPILED) {
return registry;
} else {
return override || registry;
}
}
};
/**
* @private {Map.<string, function():shaka.extern.StorageMechanism>}
*/
shaka.offline.StorageMuxer.override_ = null;
/**
* @private {!Map.<string, function():shaka.extern.StorageMechanism>}
*/
shaka.offline.StorageMuxer.registry_ = new Map();